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:
2025-12-26 22:40:52 -06:00
parent 464692ce56
commit b4b2bfe749
47 changed files with 240110 additions and 2578 deletions

33616
assets/bible_xml/ESV.xml Normal file

File diff suppressed because it is too large Load Diff

33766
assets/bible_xml/KJV.xml Normal file

File diff suppressed because it is too large Load Diff

33615
assets/bible_xml/MSG.xml Normal file

File diff suppressed because it is too large Load Diff

33614
assets/bible_xml/NASB.xml Normal file

File diff suppressed because it is too large Load Diff

33615
assets/bible_xml/NIV.xml Normal file

File diff suppressed because it is too large Load Diff

33615
assets/bible_xml/NKJV.xml Normal file

File diff suppressed because it is too large Load Diff

33617
assets/bible_xml/NLT.xml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
placeholder

208
assets/scriptures.json Normal file
View File

@@ -0,0 +1,208 @@
{
"menstrual": [
{
"verses": {
"esv": "Come to me, all who labor and are heavy laden, and I will give you rest.",
"niv": "Come to me, all you who are weary and burdened, and I will give you rest.",
"nkjv": "Come to me, all you who labor and are heavy laden, and I will give you rest.",
"nlt": "Come to me, all of you who are weary and carry heavy burdens, and I will give you rest.",
"nasb": "Come to Me, all who are weary and burdened, and I will give you rest.",
"kjv": "Come unto me, all ye that labour and are heavy laden, and I will give you rest."
},
"reference": "Matthew 11:28",
"reflection": "Your body is doing important work. Rest is not weakness—it's wisdom.",
"applicablePhases": ["menstrual"]
},
{
"verses": {
"esv": "He gives power to the faint, and to him who has no might he increases strength.",
"niv": "He gives strength to the weary and increases the power of the weak.",
"nkjv": "He gives power to the weak, and to those who have no might He increases strength.",
"nasb": "He gives strength to the weary, and to the one who lacks might He increases power."
},
"reference": "Isaiah 40:29",
"applicablePhases": ["menstrual"]
},
{
"verses": {
"esv": "The LORD is my shepherd; I shall not want. He makes me lie down in green pastures.",
"niv": "The LORD is my shepherd, I lack nothing. He makes me lie down in green pastures.",
"nkjv": "The LORD is my shepherd; I shall not want. He makes me to lie down in green pastures."
},
"reference": "Psalm 23:1-2",
"applicablePhases": ["menstrual"]
}
],
"follicular": [
{
"verses": {
"esv": "Strength and dignity are her clothing, and she laughs at the time to come.",
"niv": "She is clothed with strength and dignity; she can laugh at the days to come.",
"nkjv": "Strength and honor are her clothing; she shall rejoice in time to come.",
"nlt": "She is clothed with strength and dignity, and she laughs without fear of the future."
},
"reference": "Proverbs 31:25",
"reflection": "You're entering a season of renewed energy. Use it for His glory.",
"applicablePhases": ["follicular"]
},
{
"verses": {
"esv": "I can do all things through him who strengthens me.",
"niv": "I can do all this through him who gives me strength.",
"nkjv": "I can do all things through Christ who strengthens me."
},
"reference": "Philippians 4:13",
"applicablePhases": ["follicular"]
},
{
"verses": {
"esv": "but they who wait for the LORD shall renew their strength; they shall mount up with wings like eagles.",
"niv": "but those who hope in the LORD will renew their strength. They will soar on wings like eagles."
},
"reference": "Isaiah 40:31",
"applicablePhases": ["follicular"]
}
],
"ovulation": [
{
"verses": {
"esv": "For you formed my inmost parts; you knitted me together in my mother's womb. I praise you, for I am fearfully and wonderfully made.",
"niv": "For you created my inmost being; you knit me together in my mothers womb. I praise you because I am fearfully and wonderfully made.",
"nkjv": "For You formed my inward parts; You covered me in my mothers womb. I will praise You, for I am fearfully and wonderfully made."
},
"reference": "Psalm 139:13-14",
"reflection": "Your body reflects the incredible creativity of God.",
"applicablePhases": ["ovulation"]
},
{
"verses": {
"esv": "Behold, children are a heritage from the LORD, the fruit of the womb a reward.",
"niv": "Children are a heritage from the LORD, offspring a reward from him.",
"nkjv": "Behold, children are a heritage from the LORD, the fruit of the womb is a reward."
},
"reference": "Psalm 127:3",
"applicablePhases": ["ovulation"]
}
],
"luteal": [
{
"verses": {
"esv": "For I know the plans I have for you, declares the LORD, plans for welfare and not for evil, to give you a future and a hope.",
"niv": "For I know the plans I have for you,” declares the LORD, “plans to prosper you and not to harm you, plans to give you hope and a future.",
"nkjv": "For I know the thoughts that I think toward you, says the LORD, thoughts of peace and not of evil, to give you a future and a hope."
},
"reference": "Jeremiah 29:11",
"reflection": "Whatever this season holds, God's plans for you are good.",
"applicablePhases": ["luteal"]
},
{
"verses": {
"esv": "do not be anxious about anything, but in everything by prayer and supplication with thanksgiving let your requests be made known to God.",
"niv": "Do not be anxious about anything, but in every situation, by prayer and petition, with thanksgiving, present your requests to God."
},
"reference": "Philippians 4:6",
"applicablePhases": ["luteal"]
},
{
"verses": {
"esv": "Trust in the LORD with all your heart, and do not lean on your own understanding.",
"niv": "Trust in the LORD with all your heart and lean not on your own understanding."
},
"reference": "Proverbs 3:5",
"applicablePhases": ["luteal"]
}
],
"husband": [
{
"verses": {
"esv": "Husbands, love your wives, as Christ loved the church and gave himself up for her.",
"niv": "Husbands, love your wives, just as Christ loved the church and gave himself up for her."
},
"reference": "Ephesians 5:25",
"reflection": "Love sacrificially—putting her needs before your own."
},
{
"verses": {
"esv": "Likewise, husbands, live with your wives in an understanding way, showing honor to the woman.",
"niv": "Husbands, in the same way be considerate as you live with your wives, and treat them with respect."
},
"reference": "1 Peter 3:7"
}
],
"womanhood": [
{
"verses": {
"esv": "Charm is deceitful, and beauty is vain, but a woman who fears the LORD is to be praised.",
"niv": "Charm is deceptive, and beauty is fleeting; but a woman who fears the LORD is to be praised."
},
"reference": "Proverbs 31:30"
},
{
"verses": {
"esv": "She opens her mouth with wisdom, and the teaching of kindness is on her tongue.",
"niv": "She opens her mouth with wisdom, and the teaching of kindness is on her tongue."
},
"reference": "Proverbs 31:26"
}
],
"contextual": {
"pain": [
{
"verses": {
"esv": "The LORD is near to the brokenhearted and saves the crushed in spirit.",
"niv": "The LORD is close to the brokenhearted and saves those who are crushed in spirit."
},
"reference": "Psalm 34:18",
"reflection": "He sees your pain and draws near to you in your discomfort."
},
{
"verses": {
"esv": "Cast your burden on the LORD, and he will sustain you.",
"niv": "Cast your cares on the LORD and he will sustain you."
},
"reference": "Psalm 55:22"
}
],
"fatigue": [
{
"verses": {
"esv": "He gives power to the faint, and to him who has no might he increases strength.",
"niv": "He gives strength to the weary and increases the power of the weak."
},
"reference": "Isaiah 40:29"
},
{
"verses": {
"esv": "My grace is sufficient for you, for my power is made perfect in weakness.",
"niv": "My grace is sufficient for you, for my power is made perfect in weakness."
},
"reference": "2 Corinthians 12:9"
}
],
"anxiety": [
{
"verses": {
"esv": "When the cares of my heart are many, your consolations cheer my soul.",
"niv": "When anxiety was great within me, your consolation brought me joy."
},
"reference": "Psalm 94:19"
},
{
"verses": {
"esv": "Peace I leave with you; my peace I give to you. Not as the world gives do I give to you. Let not your hearts be troubled, neither let them be afraid.",
"niv": "Peace I leave with you; my peace I give to you. I do not give to you as the world gives. Do not let your hearts be troubled and do not be afraid."
},
"reference": "John 14:27"
}
],
"joy": [
{
"verses": {
"esv": "The LORD is my strength and my shield; in him my heart trusts, and I am helped; my heart exults, and with my song I give thanks to him.",
"niv": "The LORD is my strength and my shield; my heart trusts in him, and he helps me. My heart leaps for joy, and with my song I praise him."
},
"reference": "Psalm 28:7"
}
]
}
}

File diff suppressed because one or more lines are too long

490
lib/data/learn_content.dart Normal file
View File

@@ -0,0 +1,490 @@
/// Learn article content for the Husband section
/// Contains educational articles about understanding her cycle, biblical manhood, and NFP
class LearnArticle {
final String id;
final String title;
final String subtitle;
final String category;
final List<LearnSection> sections;
const LearnArticle({
required this.id,
required this.title,
required this.subtitle,
required this.category,
required this.sections,
});
}
class LearnSection {
final String? heading;
final String content;
const LearnSection({this.heading, required this.content});
}
/// All learn articles for the husband
class LearnContent {
static const List<LearnArticle> articles = [
// ========== UNDERSTANDING HER ==========
LearnArticle(
id: 'four_phases',
title: 'The 4 Phases of Her Cycle',
subtitle: 'What\'s happening in her body each month',
category: 'Understanding Her',
sections: [
LearnSection(
content: 'Your wife\'s body goes through four distinct phases each month, '
'each bringing different physical and emotional experiences. Understanding '
'these phases helps you be a more attentive and supportive husband.',
),
LearnSection(
heading: '1. Menstrual Phase (Days 1-5)',
content: 'This is when her period occurs. The uterine lining sheds, which can '
'cause cramping, fatigue, and lower energy. Many women experience headaches, '
'bloating, and mood sensitivity during this time.\n\n'
'💡 How you can help: Offer comfort items, help with household tasks, '
'and be patient with her energy levels.',
),
LearnSection(
heading: 'Dysmenorrhea (Painful Periods)',
content: 'While some cramping is normal, severe menstrual pain is called '
'Dysmenorrhea. It affects many women and can be truly debilitating.\n\n'
'**Primary Dysmenorrhea**: Common menstrual cramps caused by '
'prostaglandins (chemicals that make the uterus contract). Pain '
'usually starts 1-2 days before or at the start of the period.\n\n'
'**Secondary Dysmenorrhea**: Pain caused by an underlying medical '
'condition like endometriosis or fibroids. This pain often lasts '
'longer than normal cramps.\n\n'
'💡 How you can help: Provide a heating pad, offer a gentle massage, '
'ensure she has painkillers ready, and encourage her to see a doctor '
'if the pain is consistently severe.',
),
LearnSection(
heading: '2. Follicular Phase (Days 6-12)',
content: 'After her period ends, estrogen begins rising. This typically brings '
'increased energy, better mood, and higher confidence. She may feel more '
'social and creative during this time.\n\n'
'💡 How you can help: This is a great time for date nights, important '
'conversations, and planning activities together.',
),
LearnSection(
heading: '3. Ovulation Phase (Days 13-15)',
content: 'This is peak fertility. Estrogen peaks and her body releases an egg. '
'Many women feel their best during this phase—higher energy, increased '
'confidence, and feeling more attractive.\n\n'
'💡 How you can help: Appreciate and affirm her. This is when she may feel '
'most connected and intimate.',
),
LearnSection(
heading: '4. Luteal Phase (Days 16-28)',
content: 'Progesterone rises after ovulation. If pregnancy doesn\'t occur, '
'hormone levels drop, leading to PMS symptoms like mood swings, irritability, '
'fatigue, cravings, and bloating.\n\n'
'💡 How you can help: Extra patience and understanding. Her feelings are real, '
'even when they seem disproportionate to the situation.',
),
LearnSection(
heading: 'Remember',
content: '"Husbands, love your wives, just as Christ loved the church and gave '
'himself up for her." — Ephesians 5:25\n\n'
'Understanding her cycle is one practical way to live out sacrificial love.',
),
],
),
LearnArticle(
id: 'mood_changes',
title: 'Why Does Her Mood Change?',
subtitle: 'Hormones explained simply',
category: 'Understanding Her',
sections: [
LearnSection(
content: 'If you\'ve ever wondered why your wife seems like a different person '
'at different times of the month, the answer lies in hormones. These powerful '
'chemical messengers directly affect her brain, emotions, and body.',
),
LearnSection(
heading: 'The Key Hormones',
content: '**Estrogen** — The "feel good" hormone. When it\'s high, she typically '
'feels more energetic, positive, and socially engaged. When it drops suddenly '
'(before her period), it can cause mood dips.\n\n'
'**Progesterone** — The "calming" hormone. It rises after ovulation and can cause '
'sleepiness, anxiety, or feeling more emotional. When it drops before her period, '
'it contributes to PMS symptoms.\n\n'
'**Testosterone** — Yes, women have this too! It peaks around ovulation and '
'contributes to increased desire and confidence.',
),
LearnSection(
heading: 'It\'s Not "In Her Head"',
content: 'Studies show that hormonal fluctuations create real changes in brain chemistry. '
'The limbic system (emotional center) is highly sensitive to hormone levels. When '
'her hormones shift, her emotional responses genuinely change—this isn\'t weakness '
'or drama, it\'s biology.',
),
LearnSection(
heading: 'What This Means for You',
content: '• Don\'t dismiss her feelings as "just hormones"\n'
'• Recognize patterns (she may have harder days predictably)\n'
'• Adjust your expectations around her cycle\n'
'• Offer extra grace during the luteal phase and period\n'
'• Celebrate with her during her high-energy days',
),
LearnSection(
heading: 'Scripture to Remember',
content: '"Live with your wives in an understanding way, showing honor to the woman '
'as the weaker vessel, since they are heirs with you of the grace of life, so '
'that your prayers may not be hindered." — 1 Peter 3:7',
),
],
),
LearnArticle(
id: 'pms_is_real',
title: 'PMS is Real',
subtitle: 'Medical facts for supportive husbands',
category: 'Understanding Her',
sections: [
LearnSection(
content: 'Premenstrual Syndrome (PMS) affects up to 90% of women to some degree. '
'For 20-40% of women, symptoms significantly impact daily life. This is not '
'exaggeration or seeking attention—it\'s a medically recognized condition.',
),
LearnSection(
heading: 'Physical Symptoms',
content: '• Bloating and water retention (clothes may feel tight)\n'
'• Breast tenderness and swelling\n'
'• Headaches or migraines\n'
'• Fatigue and low energy\n'
'• Muscle aches and joint pain\n'
'• Cramping (can range from mild to severe)\n'
'• Changes in appetite and food cravings\n'
'• Sleep disturbances',
),
LearnSection(
heading: 'Emotional Symptoms',
content: '• Irritability and mood swings\n'
'• Sadness or crying spells\n'
'• Anxiety or tension\n'
'• Difficulty concentrating\n'
'• Feeling overwhelmed\n'
'• Decreased interest in usual activities\n'
'• Sensitivity to rejection',
),
LearnSection(
heading: 'PMDD: When It\'s Severe',
content: 'About 3-8% of women experience Premenstrual Dysphoric Disorder (PMDD), '
'a severe form of PMS. Symptoms can include depression, hopelessness, severe '
'anxiety, and difficulty functioning. If your wife experiences severe symptoms, '
'encourage her to talk to a doctor—treatment is available.',
),
LearnSection(
heading: 'How to Be Supportive',
content: '✓ Believe her when she says she doesn\'t feel well\n'
'✓ Don\'t take her irritability personally (it\'s the hormones)\n'
'✓ Offer practical help without being asked\n'
'✓ Keep comfort items available (heating pad, chocolate, tea)\n'
'✓ Give her space when she needs it\n'
'✓ Be extra gentle with your words during this time\n'
'✓ Track her cycle too so you can anticipate and prepare',
),
LearnSection(
heading: 'A Husband\'s Prayer',
content: '"Lord, help me to be patient and understanding when my wife is struggling. '
'Give me eyes to see her needs and a heart willing to serve. Help me reflect '
'Your love to her, especially when it\'s hard. Amen."',
),
],
),
// ========== BIBLICAL MANHOOD ==========
LearnArticle(
id: 'loving_like_christ',
title: 'Loving Like Christ',
subtitle: 'Ephesians 5 in daily practice',
category: 'Biblical Manhood',
sections: [
LearnSection(
content: '"Husbands, love your wives, as Christ loved the church and gave himself '
'up for her." — Ephesians 5:25\n\n'
'This is the highest calling for a husband. But what does it actually look like '
'in everyday life, especially related to her cycle?',
),
LearnSection(
heading: 'Christ\'s Love is Sacrificial',
content: 'Jesus gave up His comfort, His preferences, and ultimately His life. '
'For you, this means:\n\n'
'• Helping with housework when she\'s exhausted from cramps\n'
'• Keeping your frustration in check when she\'s emotional\n'
'• Running to the store for her needs without complaining\n'
'• Giving up your plans to care for her when she\'s unwell',
),
LearnSection(
heading: 'Christ\'s Love is Patient',
content: 'Jesus was patient with His disciples\' failures and slow understanding. '
'For you, this means:\n\n'
'• Not rushing her to "get over" how she\'s feeling\n'
'• Listening without immediately trying to fix things\n'
'• Being willing to have the same conversation again\n'
'• Accepting her where she is, not where you want her to be',
),
LearnSection(
heading: 'Christ\'s Love is Nourishing',
content: '"He nourishes and cherishes her" (Ephesians 5:29). For you, this means:\n\n'
'• Speaking words of encouragement and affirmation\n'
'• Providing for her physical comfort\n'
'• Protecting her from unnecessary stress\n'
'• Building her up, never tearing her down',
),
LearnSection(
heading: 'Christ\'s Love is Understanding',
content: 'Jesus knows our weaknesses because He experienced human flesh. '
'For you, this means:\n\n'
'• Learning about her cycle and what she experiences\n'
'• Remembering what helps her and what doesn\'t\n'
'• Anticipating her needs before she has to ask\n'
'• Never using her vulnerabilities against her',
),
LearnSection(
heading: 'Daily Application',
content: 'Each morning, ask yourself: "How can I love her like Christ today?"\n\n'
'Each evening, reflect: "Did I sacrifice my preferences for her good today?"',
),
],
),
LearnArticle(
id: 'servant_leadership',
title: 'Servant Leadership at Home',
subtitle: 'What it really means',
category: 'Biblical Manhood',
sections: [
LearnSection(
content: '"For even the Son of Man came not to be served but to serve, and to give '
'his life as a ransom for many." — Mark 10:45\n\n'
'Biblical leadership isn\'t about authority and control—it\'s about service and sacrifice.',
),
LearnSection(
heading: 'Leadership = Responsibility, Not Power',
content: 'Being the head of your home means you are responsible for:\n\n'
'• Your wife\'s spiritual well-being\n'
'• The emotional atmosphere of your home\n'
'• Initiating prayer and spiritual conversations\n'
'• Taking the first step in reconciliation after conflict\n'
'• Setting the example of humility and servanthood',
),
LearnSection(
heading: 'A Servant Leader Listens',
content: 'Great leaders listen more than they speak. When your wife is struggling:\n\n'
'• Put down your phone and give full attention\n'
'• Ask questions to understand, not to fix\n'
'• Validate her feelings before offering solutions\n'
'• Remember what she shares and follow up later',
),
LearnSection(
heading: 'A Servant Leader Protects',
content: 'You protect your wife by:\n\n'
'• Shielding her from unnecessary criticism (including from family)\n'
'• Not overloading her with expectations when she\'s struggling\n'
'• Creating a safe space for her to be vulnerable\n'
'• Standing between her and stress when you can',
),
LearnSection(
heading: 'A Servant Leader Gets His Hands Dirty',
content: 'Jesus washed feet—the work of the lowest servant. For you, this means:\n\n'
'• Doing dishes, laundry, and cleaning without being asked\n'
'• Getting up with sick children at night\n'
'• Taking on tasks she normally does when she\'s unwell\n'
'• Never considering any task "beneath you"',
),
LearnSection(
heading: 'During Her Cycle',
content: 'Servant leadership shines during difficult days:\n\n'
'• Take over dinner without complaining\n'
'• Give her permission to rest without guilt\n'
'• Handle the kids so she can have quiet time\n'
'• Be proactive about what needs to be done',
),
],
),
LearnArticle(
id: 'praying_for_wife',
title: 'Praying for Your Wife',
subtitle: 'Practical guide',
category: 'Biblical Manhood',
sections: [
LearnSection(
content: 'Prayer is the most powerful thing you can do for your wife. It changes '
'her, changes you, and invites God\'s presence into your marriage.',
),
LearnSection(
heading: 'Pray for Her Physical Well-being',
content: 'Especially during her period or difficult days:\n\n'
'"Lord, ease her pain and give her body rest. Help her cycle be regular '
'and her symptoms manageable. Give her strength for each day."',
),
LearnSection(
heading: 'Pray for Her Emotions',
content: 'When hormones make everything feel harder:\n\n'
'"Father, stabilize her emotions and give her peace. When anxiety rises, '
'remind her of Your presence. When sadness comes, comfort her spirit."',
),
LearnSection(
heading: 'Pray for Her Spirit',
content: 'For her walk with God:\n\n'
'"Lord, draw her close to You. Give her hunger for Your Word. Fill her '
'with Your Spirit and help her bear fruit in every season."',
),
LearnSection(
heading: 'Pray for Your Marriage',
content: 'For unity and connection:\n\n'
'"God, help us to be unified in heart and purpose. Give us patience with each other. '
'Deepen our intimacy—emotionally, spiritually, and physically. Help me love '
'her as Christ loves the church."',
),
LearnSection(
heading: 'Pray for Yourself',
content: 'To be the husband she needs:\n\n'
'"Lord, make me a better husband. Give me patience when I\'m frustrated. '
'Help me see her needs and meet them with joy. Transform my selfishness '
'into servant-hearted love."',
),
LearnSection(
heading: 'Practical Tips',
content: '• Pray for her daily, even briefly\n'
'• Pray with her when you can (hold hands and pray together)\n'
'• Ask her how you can pray for her specifically\n'
'• Pray during her difficult days with extra intention\n'
'• Write out prayers and save them\n'
'• Use the "Pray for [Her Name]" feature in this app',
),
],
),
// ========== NFP FOR HUSBANDS ==========
LearnArticle(
id: 'reading_charts',
title: 'Reading the Charts Together',
subtitle: 'Understanding fertility signs',
category: 'NFP for Husbands',
sections: [
LearnSection(
content: 'Natural Family Planning (NFP) isn\'t just your wife\'s responsibility—it\'s '
'a shared journey. Learning to understand her fertility signs brings you closer '
'together and honors the gift of her body.',
),
LearnSection(
heading: 'The Main Fertility Signs',
content: '**Cervical Mucus**: As ovulation approaches, mucus changes from dry to '
'wet, slippery, and stretchy (like egg whites). This indicates peak fertility.\n\n'
'**Basal Body Temperature**: Temperature rises 0.5-1°F after ovulation and stays '
'elevated until the next period. A sustained rise confirms ovulation occurred.\n\n'
'**Cervical Position**: The cervix softens and opens around ovulation. Your wife '
'may track this for additional confirmation.',
),
LearnSection(
heading: 'Understanding the Chart',
content: 'Most NFP charts show:\n\n'
'• Daily temperature readings\n'
'• Mucus observations (symbols like circles, lines, or colors)\n'
'• Cycle day numbers\n'
'• Phase of the cycle\n'
'• Fertile window (usually shaded or highlighted)',
),
LearnSection(
heading: 'Why It Matters for Husbands',
content: 'When you understand the chart:\n\n'
'• You can participate in family planning decisions together\n'
'• You recognize when sacrifice (abstinence) is needed\n'
'• You appreciate what her body goes through each month\n'
'• You can anticipate her emotional and physical changes\n'
'• Intimacy decisions become a shared discernment, not a conflict',
),
LearnSection(
heading: 'How to Support',
content: '• Ask her to explain the chart to you\n'
'• Check in on what phase she\'s in\n'
'• Don\'t make her feel like she has to track alone\n'
'• Thank her for the effort she puts into charting\n'
'• Use this app to follow along with her cycle',
),
LearnSection(
heading: 'Scripture',
content: '"Two are better than one, because they have a good reward for their toil. '
'For if they fall, one will lift up his fellow." — Ecclesiastes 4:9-10',
),
],
),
LearnArticle(
id: 'abstinence_discipline',
title: 'Abstinence as Spiritual Discipline',
subtitle: 'Growing together during fertile days',
category: 'NFP for Husbands',
sections: [
LearnSection(
content: 'For couples practicing NFP to avoid pregnancy, abstaining during the '
'fertile window can be challenging. But this sacrifice can become a profound '
'spiritual discipline that strengthens your marriage.',
),
LearnSection(
heading: 'The Challenge is Real',
content: 'Let\'s be honest: abstinence during fertile days is difficult.\n\n'
'• Her desire may peak during ovulation (biology is ironic that way)\n'
'• It requires self-control and communication\n'
'• It can feel like a sacrifice without immediate reward\n\n'
'This is not meant to be easy. But difficult things often bring the greatest growth.',
),
LearnSection(
heading: 'Reframing Abstinence',
content: 'Instead of seeing fertile days as deprivation, see them as:\n\n'
'• **An opportunity for self-control**: "Like a city whose walls are broken through '
'is a person who lacks self-control." (Proverbs 25:28)\n\n'
'• **A chance to serve your wife**: Respecting her body and your shared decision\n\n'
'• **A time to deepen non-physical intimacy**: Emotional connection, conversation, prayer\n\n'
'• **A reminder that sex is a gift, not a right**: Gratitude increases appreciation',
),
LearnSection(
heading: 'Ways to Stay Connected',
content: 'During fertile days, intentionally build closeness through:\n\n'
'• Quality conversation (deeper than logistics)\n'
'• Physical affection without intercourse\n'
'• Date nights focused on connection\n'
'• Praying together\n'
'• Acts of service for each other\n'
'• Planning future intimacy (anticipation can be sweet)',
),
LearnSection(
heading: 'The Payoff',
content: 'Couples who practice periodic abstinence often report:\n\n'
'• Increased appreciation for each other\n'
'• Deeper emotional intimacy\n'
'• Better communication about desires and needs\n'
'• A sense of shared sacrifice and teamwork\n'
'• Renewal and excitement when intimacy resumes',
),
LearnSection(
heading: 'A Prayer for Difficult Days',
content: '"Lord, this is hard. Help me to love her well even when I\'m frustrated. '
'Give me self-control and help me see this sacrifice as an offering to You. '
'Deepen our connection in other ways during this time. Amen."',
),
],
),
];
/// Get an article by ID
static LearnArticle? getArticle(String id) {
try {
return articles.firstWhere((a) => a.id == id);
} catch (_) {
return null;
}
}
}

View File

@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:google_fonts/google_fonts.dart';
import 'models/scripture.dart';
import 'theme/app_theme.dart';
import 'screens/splash_screen.dart';
import 'models/user_profile.dart';
@@ -10,10 +11,10 @@ import 'models/cycle_entry.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Hive for local storage
await Hive.initFlutter();
// Register Hive adapters
Hive.registerAdapter(UserProfileAdapter());
Hive.registerAdapter(CycleEntryAdapter());
@@ -24,15 +25,16 @@ void main() async {
Hive.registerAdapter(CervicalMucusTypeAdapter());
Hive.registerAdapter(CyclePhaseAdapter());
Hive.registerAdapter(UserRoleAdapter());
<<<<<<< HEAD
=======
Hive.registerAdapter(BibleTranslationAdapter());
>>>>>>> 6742220 (Your commit message here)
// Open boxes
await Hive.openBox<UserProfile>('user_profile');
await Hive.openBox<CycleEntry>('cycle_entries');
Hive.registerAdapter(ScriptureAdapter()); // Register Scripture adapter
// Open boxes and load scriptures in parallel
await Future.wait([
Hive.openBox<UserProfile>('user_profile'),
Hive.openBox<CycleEntry>('cycle_entries'),
ScriptureDatabase().loadScriptures(),
]);
runApp(const ProviderScope(child: ChristianPeriodTrackerApp()));
}

View File

@@ -1,9 +1,7 @@
import 'package:hive/hive.dart';
<<<<<<< HEAD
=======
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import '../theme/app_theme.dart';
>>>>>>> 6742220 (Your commit message here)
part 'cycle_entry.g.dart';
@@ -12,16 +10,16 @@ part 'cycle_entry.g.dart';
enum MoodLevel {
@HiveField(0)
verySad,
@HiveField(1)
sad,
@HiveField(2)
neutral,
@HiveField(3)
happy,
@HiveField(4)
veryHappy,
}
@@ -31,13 +29,13 @@ enum MoodLevel {
enum FlowIntensity {
@HiveField(0)
spotting,
@HiveField(1)
light,
@HiveField(2)
medium,
@HiveField(3)
heavy,
}
@@ -47,16 +45,16 @@ enum FlowIntensity {
enum CervicalMucusType {
@HiveField(0)
dry,
@HiveField(1)
sticky,
@HiveField(2)
creamy,
@HiveField(3)
eggWhite,
@HiveField(4)
watery,
}
@@ -66,13 +64,13 @@ enum CervicalMucusType {
enum CyclePhase {
@HiveField(0)
menstrual,
@HiveField(1)
follicular,
@HiveField(2)
ovulation,
@HiveField(3)
luteal,
}
@@ -82,88 +80,94 @@ enum CyclePhase {
class CycleEntry extends HiveObject {
@HiveField(0)
String id;
@HiveField(1)
DateTime date;
@HiveField(2)
bool isPeriodDay;
@HiveField(3)
FlowIntensity? flowIntensity;
@HiveField(4)
MoodLevel? mood;
@HiveField(5)
int? energyLevel; // 1-5
@HiveField(6)
int? crampIntensity; // 1-5
@HiveField(7)
bool hasHeadache;
@HiveField(8)
bool hasBloating;
@HiveField(9)
bool hasBreastTenderness;
@HiveField(10)
bool hasFatigue;
@HiveField(11)
bool hasAcne;
<<<<<<< HEAD
=======
@HiveField(22)
bool hasLowerBackPain;
@HiveField(23)
bool hasConstipation;
@HiveField(24)
bool hasDiarrhea;
@HiveField(25)
int? stressLevel; // 1-5
@HiveField(26)
bool hasInsomnia;
>>>>>>> 6742220 (Your commit message here)
@HiveField(12)
double? basalBodyTemperature; // in Fahrenheit
@HiveField(13)
CervicalMucusType? cervicalMucus;
@HiveField(14)
bool? ovulationTestPositive;
@HiveField(15)
String? notes;
@HiveField(16)
int? sleepHours;
@HiveField(17)
int? waterIntake; // glasses
@HiveField(18)
bool hadExercise;
@HiveField(19)
bool hadIntimacy; // For married users only
@HiveField(20)
DateTime createdAt;
@HiveField(21)
DateTime updatedAt;
@HiveField(27)
List<String>? cravings;
@HiveField(28)
String? husbandNotes; // Separate notes for husband
@HiveField(29)
bool? intimacyProtected; // null = no intimacy, true = protected, false = unprotected
CycleEntry({
required this.id,
required this.date,
@@ -177,71 +181,54 @@ class CycleEntry extends HiveObject {
this.hasBreastTenderness = false,
this.hasFatigue = false,
this.hasAcne = false,
<<<<<<< HEAD
=======
this.hasLowerBackPain = false,
this.hasConstipation = false,
this.hasDiarrhea = false,
this.stressLevel,
this.hasInsomnia = false,
>>>>>>> 6742220 (Your commit message here)
this.basalBodyTemperature,
this.cervicalMucus,
this.ovulationTestPositive,
this.notes,
this.cravings,
this.sleepHours,
this.waterIntake,
this.hadExercise = false,
this.hadIntimacy = false,
this.intimacyProtected,
required this.createdAt,
required this.updatedAt,
this.husbandNotes,
});
List<bool> get _symptomsList => [
hasHeadache,
hasBloating,
hasBreastTenderness,
hasFatigue,
hasAcne,
hasLowerBackPain,
hasConstipation,
hasDiarrhea,
hasInsomnia,
(crampIntensity != null && crampIntensity! > 0),
(stressLevel != null && stressLevel! > 1),
];
/// Check if any symptoms are logged
bool get hasSymptoms =>
hasHeadache ||
hasBloating ||
hasBreastTenderness ||
hasFatigue ||
hasAcne ||
<<<<<<< HEAD
(crampIntensity != null && crampIntensity! > 0);
=======
hasLowerBackPain ||
hasConstipation ||
hasDiarrhea ||
hasInsomnia ||
(crampIntensity != null && crampIntensity! > 0) ||
(stressLevel != null && stressLevel! > 1);
>>>>>>> 6742220 (Your commit message here)
bool get hasSymptoms => _symptomsList.contains(true);
/// Check if NFP data is logged
bool get hasNFPData =>
basalBodyTemperature != null ||
cervicalMucus != null ||
ovulationTestPositive != null;
/// Get symptom count
int get symptomCount {
int count = 0;
if (hasHeadache) count++;
if (hasBloating) count++;
if (hasBreastTenderness) count++;
if (hasFatigue) count++;
if (hasAcne) count++;
<<<<<<< HEAD
if (crampIntensity != null && crampIntensity! > 0) count++;
=======
if (hasLowerBackPain) count++;
if (hasConstipation) count++;
if (hasDiarrhea) count++;
if (hasInsomnia) count++;
if (crampIntensity != null && crampIntensity! > 0) count++;
if (stressLevel != null && stressLevel! > 1) count++;
>>>>>>> 6742220 (Your commit message here)
return count;
}
int get symptomCount => _symptomsList.where((s) => s).length;
// ... (omitted getters)
/// Copy with updated fields
CycleEntry copyWith({
String? id,
@@ -256,24 +243,24 @@ class CycleEntry extends HiveObject {
bool? hasBreastTenderness,
bool? hasFatigue,
bool? hasAcne,
<<<<<<< HEAD
=======
bool? hasLowerBackPain,
bool? hasConstipation,
bool? hasDiarrhea,
int? stressLevel,
bool? hasInsomnia,
>>>>>>> 6742220 (Your commit message here)
double? basalBodyTemperature,
CervicalMucusType? cervicalMucus,
bool? ovulationTestPositive,
String? notes,
List<String>? cravings,
int? sleepHours,
int? waterIntake,
bool? hadExercise,
bool? hadIntimacy,
bool? intimacyProtected,
DateTime? createdAt,
DateTime? updatedAt,
String? husbandNotes,
}) {
return CycleEntry(
id: id ?? this.id,
@@ -288,24 +275,24 @@ class CycleEntry extends HiveObject {
hasBreastTenderness: hasBreastTenderness ?? this.hasBreastTenderness,
hasFatigue: hasFatigue ?? this.hasFatigue,
hasAcne: hasAcne ?? this.hasAcne,
<<<<<<< HEAD
=======
hasLowerBackPain: hasLowerBackPain ?? this.hasLowerBackPain,
hasConstipation: hasConstipation ?? this.hasConstipation,
hasDiarrhea: hasDiarrhea ?? this.hasDiarrhea,
stressLevel: stressLevel ?? this.stressLevel,
hasInsomnia: hasInsomnia ?? this.hasInsomnia,
>>>>>>> 6742220 (Your commit message here)
basalBodyTemperature: basalBodyTemperature ?? this.basalBodyTemperature,
cervicalMucus: cervicalMucus ?? this.cervicalMucus,
ovulationTestPositive: ovulationTestPositive ?? this.ovulationTestPositive,
notes: notes ?? this.notes,
cravings: cravings ?? this.cravings,
sleepHours: sleepHours ?? this.sleepHours,
waterIntake: waterIntake ?? this.waterIntake,
hadExercise: hadExercise ?? this.hadExercise,
hadIntimacy: hadIntimacy ?? this.hadIntimacy,
intimacyProtected: intimacyProtected ?? this.intimacyProtected,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? DateTime.now(),
husbandNotes: husbandNotes ?? this.husbandNotes,
);
}
}
@@ -326,7 +313,7 @@ extension MoodLevelExtension on MoodLevel {
return '😄';
}
}
String get label {
switch (this) {
case MoodLevel.verySad:
@@ -371,7 +358,7 @@ extension CyclePhaseExtension on CyclePhase {
return 'Luteal';
}
}
String get emoji {
switch (this) {
case CyclePhase.menstrual:
@@ -384,8 +371,6 @@ extension CyclePhaseExtension on CyclePhase {
return '🌙';
}
}
<<<<<<< HEAD
=======
Color get color {
switch (this) {
@@ -405,15 +390,22 @@ extension CyclePhaseExtension on CyclePhase {
case CyclePhase.menstrual:
return [AppColors.rose, AppColors.menstrualPhase, AppColors.blushPink];
case CyclePhase.follicular:
return [AppColors.sageGreen, AppColors.follicularPhase, AppColors.sageGreen.withOpacity(0.7)];
return [
AppColors.sageGreen,
AppColors.follicularPhase,
AppColors.sageGreen.withOpacity(0.7)
];
case CyclePhase.ovulation:
return [AppColors.lavender, AppColors.ovulationPhase, AppColors.rose];
case CyclePhase.luteal:
return [AppColors.lutealPhase, AppColors.lavender, AppColors.blushPink];
return [
AppColors.lutealPhase,
AppColors.lavender,
AppColors.blushPink
];
}
}
>>>>>>> 6742220 (Your commit message here)
String get description {
switch (this) {
case CyclePhase.menstrual:

View File

@@ -29,35 +29,31 @@ class CycleEntryAdapter extends TypeAdapter<CycleEntry> {
hasBreastTenderness: fields[9] as bool,
hasFatigue: fields[10] as bool,
hasAcne: fields[11] as bool,
<<<<<<< HEAD
=======
hasLowerBackPain: fields[22] as bool,
hasConstipation: fields[23] as bool,
hasDiarrhea: fields[24] as bool,
stressLevel: fields[25] as int?,
hasInsomnia: fields[26] as bool,
>>>>>>> 6742220 (Your commit message here)
basalBodyTemperature: fields[12] as double?,
cervicalMucus: fields[13] as CervicalMucusType?,
ovulationTestPositive: fields[14] as bool?,
notes: fields[15] as String?,
cravings: (fields[27] as List?)?.cast<String>(),
sleepHours: fields[16] as int?,
waterIntake: fields[17] as int?,
hadExercise: fields[18] as bool,
hadIntimacy: fields[19] as bool,
intimacyProtected: fields[29] as bool?,
createdAt: fields[20] as DateTime,
updatedAt: fields[21] as DateTime,
husbandNotes: fields[28] as String?,
);
}
@override
void write(BinaryWriter writer, CycleEntry obj) {
writer
<<<<<<< HEAD
..writeByte(22)
=======
..writeByte(27)
>>>>>>> 6742220 (Your commit message here)
..writeByte(30)
..writeByte(0)
..write(obj.id)
..writeByte(1)
@@ -82,8 +78,6 @@ class CycleEntryAdapter extends TypeAdapter<CycleEntry> {
..write(obj.hasFatigue)
..writeByte(11)
..write(obj.hasAcne)
<<<<<<< HEAD
=======
..writeByte(22)
..write(obj.hasLowerBackPain)
..writeByte(23)
@@ -94,7 +88,6 @@ class CycleEntryAdapter extends TypeAdapter<CycleEntry> {
..write(obj.stressLevel)
..writeByte(26)
..write(obj.hasInsomnia)
>>>>>>> 6742220 (Your commit message here)
..writeByte(12)
..write(obj.basalBodyTemperature)
..writeByte(13)
@@ -114,7 +107,13 @@ class CycleEntryAdapter extends TypeAdapter<CycleEntry> {
..writeByte(20)
..write(obj.createdAt)
..writeByte(21)
..write(obj.updatedAt);
..write(obj.updatedAt)
..writeByte(27)
..write(obj.cravings)
..writeByte(28)
..write(obj.husbandNotes)
..writeByte(29)
..write(obj.intimacyProtected);
}
@override

View File

@@ -1,469 +1,541 @@
<<<<<<< HEAD
/// Scripture model for daily verses and devotionals
class Scripture {
final String verse;
=======
import 'dart:convert';
import 'dart:math';
import 'package:flutter/services.dart';
import 'package:hive_flutter/hive_flutter.dart'; // Import Hive
import '../services/bible_xml_parser.dart'; // Import the XML parser
import 'cycle_entry.dart';
import 'user_profile.dart';
part 'scripture.g.dart'; // Hive generated adapter
/// Scripture model for daily verses and devotionals
class Scripture {
@HiveType(typeId: 10) // Unique typeId for Scripture
class Scripture extends HiveObject {
@HiveField(0)
final Map<BibleTranslation, String> verses;
>>>>>>> 6742220 (Your commit message here)
@HiveField(1)
final String reference;
@HiveField(2)
final String? reflection;
@HiveField(3)
final List<String> applicablePhases;
@HiveField(4)
final List<String> applicableContexts;
const Scripture({
<<<<<<< HEAD
required this.verse,
=======
Scripture({
required this.verses,
>>>>>>> 6742220 (Your commit message here)
required this.reference,
this.reflection,
this.applicablePhases = const [],
this.applicableContexts = const [],
});
<<<<<<< HEAD
=======
factory Scripture.fromJson(Map<String, dynamic> json) {
return Scripture(
verses: (json['verses'] as Map<String, dynamic>).map((key, value) =>
MapEntry(
BibleTranslation.values.firstWhere((e) => e.name == key), value)),
reference: json['reference'],
reflection: json['reflection'],
applicablePhases: (json['applicablePhases'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
applicableContexts: (json['applicableContexts'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
);
}
String getVerse(BibleTranslation translation) {
return verses[translation] ?? verses[BibleTranslation.esv] ?? verses.values.first;
return verses[translation] ??
verses[BibleTranslation.esv] ??
verses.values.first;
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is Scripture &&
runtimeType == other.runtimeType &&
reference == other.reference &&
reflection == other.reflection &&
_listEquals(applicablePhases, other.applicablePhases) &&
_listEquals(applicableContexts, other.applicableContexts) &&
_mapEquals(verses, other.verses));
@override
int get hashCode =>
reference.hashCode ^
reflection.hashCode ^
Object.hashAll(applicablePhases) ^
Object.hashAll(applicableContexts) ^
Object.hashAll(verses.entries) ^
reflection.hashCode;
// Helper for list equality check
static bool _listEquals<T>(List<T>? a, List<T>? b) {
if (a == null) return b == null;
if (b == null) return false;
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
// Helper for map equality check
static bool _mapEquals<K, V>(Map<K, V>? a, Map<K, V>? b) {
if (a == null) return b == null;
if (b == null) return false;
if (a.length != b.length) return false;
if (a.keys.length != b.keys.length) return false; // Added length check
for (final key in a.keys) {
if (!b.containsKey(key) || a[key] != b[key]) return false; // Added containsKey check
}
return true;
}
>>>>>>> 6742220 (Your commit message here)
}
/// Pre-defined scriptures for the app
class ScriptureDatabase {
/// Scriptures for menstrual phase (rest, comfort)
static const List<Scripture> menstrualScriptures = [
Scripture(
<<<<<<< HEAD
verse: "Come to me, all you who are weary and burdened, and I will give you rest.",
=======
verses: {
BibleTranslation.esv: "Come to me, all who labor and are heavy laden, and I will give you rest.",
BibleTranslation.niv: "Come to me, all you who are weary and burdened, and I will give you rest.",
BibleTranslation.nkjv: "Come to me, all you who labor and are heavy laden, and I will give you rest.",
BibleTranslation.nlt: "Come to me, all of you who are weary and carry heavy burdens, and I will give you rest.",
BibleTranslation.nasb: "Come to Me, all who are weary and burdened, and I will give you rest.",
BibleTranslation.kjv: "Come unto me, all ye that labour and are heavy laden, and I will give you rest.",
},
>>>>>>> 6742220 (Your commit message here)
reference: "Matthew 11:28",
reflection: "Your body is doing important work. Rest is not weakness—it's wisdom.",
applicablePhases: ['menstrual'],
),
Scripture(
<<<<<<< HEAD
verse: "He gives strength to the weary and increases the power of the weak.",
=======
verses: {
BibleTranslation.esv: "He gives power to the faint, and to him who has no might he increases strength.",
BibleTranslation.niv: "He gives strength to the weary and increases the power of the weak.",
BibleTranslation.nkjv: "He gives power to the weak, and to those who have no might He increases strength.",
BibleTranslation.nasb: "He gives strength to the weary, and to the one who lacks might He increases power.",
},
>>>>>>> 6742220 (Your commit message here)
reference: "Isaiah 40:29",
applicablePhases: ['menstrual'],
),
Scripture(
<<<<<<< HEAD
verse: "The Lord is my shepherd; I shall not want. He makes me lie down in green pastures.",
reference: "Psalm 23:1-2",
applicablePhases: ['menstrual'],
),
Scripture(
verse: "Be still, and know that I am God.",
reference: "Psalm 46:10",
reflection: "Use this time of slowing down to be present with God.",
applicablePhases: ['menstrual'],
),
Scripture(
verse: "My grace is sufficient for you, for my power is made perfect in weakness.",
reference: "2 Corinthians 12:9",
applicablePhases: ['menstrual'],
),
=======
verses: {
BibleTranslation.esv: "The LORD is my shepherd; I shall not want. He makes me lie down in green pastures.",
BibleTranslation.niv: "The LORD is my shepherd, I lack nothing. He makes me lie down in green pastures.",
BibleTranslation.nkjv: "The LORD is my shepherd; I shall not want. He makes me to lie down in green pastures.",
},
reference: "Psalm 23:1-2",
applicablePhases: ['menstrual'],
),
>>>>>>> 6742220 (Your commit message here)
];
/// Scriptures for follicular phase (renewal, strength)
static const List<Scripture> follicularScriptures = [
Scripture(
<<<<<<< HEAD
verse: "She is clothed with strength and dignity; she can laugh at the days to come.",
=======
verses: {
BibleTranslation.esv: "Strength and dignity are her clothing, and she laughs at the time to come.",
BibleTranslation.niv: "She is clothed with strength and dignity; she can laugh at the days to come.",
BibleTranslation.nkjv: "Strength and honor are her clothing; she shall rejoice in time to come.",
BibleTranslation.nlt: "She is clothed with strength and dignity, and she laughs without fear of the future.",
},
>>>>>>> 6742220 (Your commit message here)
reference: "Proverbs 31:25",
reflection: "You're entering a season of renewed energy. Use it for His glory.",
applicablePhases: ['follicular'],
),
Scripture(
<<<<<<< HEAD
verse: "I can do all this through him who gives me strength.",
=======
verses: {
BibleTranslation.esv: "I can do all things through him who strengthens me.",
BibleTranslation.niv: "I can do all this through him who gives me strength.",
BibleTranslation.nkjv: "I can do all things through Christ who strengthens me.",
},
>>>>>>> 6742220 (Your commit message here)
reference: "Philippians 4:13",
applicablePhases: ['follicular'],
),
Scripture(
<<<<<<< HEAD
verse: "But those who hope in the Lord will renew their strength. They will soar on wings like eagles.",
reference: "Isaiah 40:31",
applicablePhases: ['follicular'],
),
Scripture(
verse: "This is the day the Lord has made; let us rejoice and be glad in it.",
reference: "Psalm 118:24",
applicablePhases: ['follicular'],
),
Scripture(
verse: "The Lord your God is with you, the Mighty Warrior who saves.",
reference: "Zephaniah 3:17",
applicablePhases: ['follicular'],
),
=======
verses: {
BibleTranslation.esv: "but they who wait for the LORD shall renew their strength; they shall mount up with wings like eagles.",
BibleTranslation.niv: "but those who hope in the LORD will renew their strength. They will soar on wings like eagles.",
},
reference: "Isaiah 40:31",
applicablePhases: ['follicular'],
),
>>>>>>> 6742220 (Your commit message here)
];
/// Scriptures for ovulation phase (creation, beauty)
static const List<Scripture> ovulationScriptures = [
Scripture(
<<<<<<< HEAD
verse: "For you created my inmost being; you knit me together in my mother's womb. I praise you because I am fearfully and wonderfully made.",
=======
verses: {
BibleTranslation.esv: "For you formed my inmost parts; you knitted me together in my mother's womb. I praise you, for I am fearfully and wonderfully made.",
BibleTranslation.niv: "For you created my inmost being; you knit me together in my mothers womb. I praise you because I am fearfully and wonderfully made.",
BibleTranslation.nkjv: "For You formed my inward parts; You covered me in my mothers womb. I will praise You, for I am fearfully and wonderfully made.",
},
>>>>>>> 6742220 (Your commit message here)
reference: "Psalm 139:13-14",
reflection: "Your body reflects the incredible creativity of God.",
applicablePhases: ['ovulation'],
),
Scripture(
<<<<<<< HEAD
verse: "Children are a heritage from the Lord, offspring a reward from him.",
reference: "Psalm 127:3",
applicablePhases: ['ovulation'],
),
Scripture(
verse: "See, I am doing a new thing! Now it springs up; do you not perceive it?",
reference: "Isaiah 43:19",
applicablePhases: ['ovulation'],
),
Scripture(
verse: "Every good and perfect gift is from above.",
reference: "James 1:17",
applicablePhases: ['ovulation'],
),
=======
verses: {
BibleTranslation.esv: "Behold, children are a heritage from the LORD, the fruit of the womb a reward.",
BibleTranslation.niv: "Children are a heritage from the LORD, offspring a reward from him.",
BibleTranslation.nkjv: "Behold, children are a heritage from the LORD, the fruit of the womb is a reward.",
},
reference: "Psalm 127:3",
applicablePhases: ['ovulation'],
),
>>>>>>> 6742220 (Your commit message here)
];
/// Scriptures for luteal phase / TWW (patience, trust)
static const List<Scripture> lutealScriptures = [
Scripture(
<<<<<<< HEAD
verse: "For I know the plans I have for you, declares the Lord, plans to prosper you and not to harm you, plans to give you hope and a future.",
=======
verses: {
BibleTranslation.esv: "For I know the plans I have for you, declares the LORD, plans for welfare and not for evil, to give you a future and a hope.",
BibleTranslation.niv: "For I know the plans I have for you,” declares the LORD, “plans to prosper you and not to harm you, plans to give you hope and a future.",
BibleTranslation.nkjv: "For I know the thoughts that I think toward you, says the LORD, thoughts of peace and not of evil, to give you a future and a hope.",
},
>>>>>>> 6742220 (Your commit message here)
reference: "Jeremiah 29:11",
reflection: "Whatever this season holds, God's plans for you are good.",
applicablePhases: ['luteal'],
),
Scripture(
<<<<<<< HEAD
verse: "Do not be anxious about anything, but in every situation, by prayer and petition, with thanksgiving, present your requests to God.",
=======
verses: {
BibleTranslation.esv: "do not be anxious about anything, but in everything by prayer and supplication with thanksgiving let your requests be made known to God.",
BibleTranslation.niv: "Do not be anxious about anything, but in every situation, by prayer and petition, with thanksgiving, present your requests to God.",
},
>>>>>>> 6742220 (Your commit message here)
reference: "Philippians 4:6",
applicablePhases: ['luteal'],
),
Scripture(
<<<<<<< HEAD
verse: "Trust in the Lord with all your heart and lean not on your own understanding.",
reference: "Proverbs 3:5",
applicablePhases: ['luteal'],
),
Scripture(
verse: "The Lord is close to the brokenhearted and saves those who are crushed in spirit.",
reference: "Psalm 34:18",
applicablePhases: ['luteal'],
),
Scripture(
verse: "And the peace of God, which transcends all understanding, will guard your hearts and your minds in Christ Jesus.",
reference: "Philippians 4:7",
applicablePhases: ['luteal'],
),
Scripture(
verse: "Wait for the Lord; be strong and take heart and wait for the Lord.",
reference: "Psalm 27:14",
applicablePhases: ['luteal'],
),
=======
verses: {
BibleTranslation.esv: "Trust in the LORD with all your heart, and do not lean on your own understanding.",
BibleTranslation.niv: "Trust in the LORD with all your heart and lean not on your own understanding.",
},
reference: "Proverbs 3:5",
applicablePhases: ['luteal'],
),
>>>>>>> 6742220 (Your commit message here)
];
/// Scriptures for husbands
static const List<Scripture> husbandScriptures = [
Scripture(
<<<<<<< HEAD
verse: "Husbands, love your wives, just as Christ loved the church and gave himself up for her.",
=======
verses: {
BibleTranslation.esv: "Husbands, love your wives, as Christ loved the church and gave himself up for her.",
BibleTranslation.niv: "Husbands, love your wives, just as Christ loved the church and gave himself up for her.",
},
>>>>>>> 6742220 (Your commit message here)
reference: "Ephesians 5:25",
reflection: "Love sacrificially—putting her needs before your own.",
),
Scripture(
<<<<<<< HEAD
verse: "Husbands, in the same way be considerate as you live with your wives, and treat them with respect.",
reference: "1 Peter 3:7",
),
Scripture(
verse: "Two are better than one, because they have a good return for their labor.",
reference: "Ecclesiastes 4:9",
),
Scripture(
verse: "Be completely humble and gentle; be patient, bearing with one another in love.",
reference: "Ephesians 4:2",
),
Scripture(
verse: "Above all, love each other deeply, because love covers over a multitude of sins.",
reference: "1 Peter 4:8",
),
Scripture(
verse: "A husband should fulfill his duty to his wife.",
reference: "1 Corinthians 7:3",
),
Scripture(
verse: "He who finds a wife finds what is good and receives favor from the Lord.",
reference: "Proverbs 18:22",
),
=======
verses: {
BibleTranslation.esv: "Likewise, husbands, live with your wives in an understanding way, showing honor to the woman.",
BibleTranslation.niv: "Husbands, in the same way be considerate as you live with your wives, and treat them with respect.",
},
reference: "1 Peter 3:7",
),
>>>>>>> 6742220 (Your commit message here)
];
/// General womanhood scriptures
static const List<Scripture> womanhoodScriptures = [
Scripture(
<<<<<<< HEAD
verse: "Charm is deceptive, and beauty is fleeting; but a woman who fears the Lord is to be praised.",
reference: "Proverbs 31:30",
),
Scripture(
verse: "She opens her mouth with wisdom, and the teaching of kindness is on her tongue.",
reference: "Proverbs 31:26",
),
Scripture(
verse: "Your beauty should not come from outward adornment... Rather, it should be that of your inner self, the unfading beauty of a gentle and quiet spirit.",
reference: "1 Peter 3:3-4",
),
Scripture(
verse: "God is within her, she will not fall; God will help her at break of day.",
reference: "Psalm 46:5",
),
];
=======
verses: {
BibleTranslation.esv: "Charm is deceitful, and beauty is vain, but a woman who fears the LORD is to be praised.",
BibleTranslation.niv: "Charm is deceptive, and beauty is fleeting; but a woman who fears the LORD is to be praised.",
},
reference: "Proverbs 31:30",
),
Scripture(
verses: {
BibleTranslation.esv: "She opens her mouth with wisdom, and the teaching of kindness is on her tongue.",
BibleTranslation.niv: "She opens her mouth with wisdom, and the teaching of kindness is on her tongue.",
},
reference: "Proverbs 31:26",
),
];
/// Scriptures for specific needs (contextual)
static const Map<String, List<Scripture>> contextualScriptures = {
'pain': [
Scripture(
verses: {
BibleTranslation.esv: "The LORD is near to the brokenhearted and saves the crushed in spirit.",
BibleTranslation.niv: "The LORD is close to the brokenhearted and saves those who are crushed in spirit.",
},
reference: "Psalm 34:18",
reflection: "He sees your pain and draws near to you in your discomfort.",
),
Scripture(
verses: {
BibleTranslation.esv: "Cast your burden on the LORD, and he will sustain you.",
BibleTranslation.niv: "Cast your cares on the LORD and he will sustain you.",
},
reference: "Psalm 55:22",
),
],
'fatigue': [
Scripture(
verses: {
BibleTranslation.esv: "He gives power to the faint, and to him who has no might he increases strength.",
BibleTranslation.niv: "He gives strength to the weary and increases the power of the weak.",
},
reference: "Isaiah 40:29",
),
Scripture(
verses: {
BibleTranslation.esv: "My grace is sufficient for you, for my power is made perfect in weakness.",
BibleTranslation.niv: "My grace is sufficient for you, for my power is made perfect in weakness.",
},
reference: "2 Corinthians 12:9",
),
],
'anxiety': [
Scripture(
verses: {
BibleTranslation.esv: "When the cares of my heart are many, your consolations cheer my soul.",
BibleTranslation.niv: "When anxiety was great within me, your consolation brought me joy.",
},
reference: "Psalm 94:19",
),
Scripture(
verses: {
BibleTranslation.esv: "Peace I leave with you; my peace I give to you. Not as the world gives do I give to you. Let not your hearts be troubled, neither let them be afraid.",
BibleTranslation.niv: "Peace I leave with you; my peace I give to you. I do not give to you as the world gives. Do not let your hearts be troubled and do not be afraid.",
},
reference: "John 14:27",
),
],
'joy': [
Scripture(
verses: {
BibleTranslation.esv: "The LORD is my strength and my shield; in him my heart trusts, and I am helped; my heart exults, and with my song I give thanks to him.",
BibleTranslation.niv: "The LORD is my strength and my shield; my heart trusts in him, and he helps me. My heart leaps for joy, and with my song I praise him.",
},
reference: "Psalm 28:7",
),
],
static final ScriptureDatabase _instance = ScriptureDatabase._internal();
factory ScriptureDatabase({BibleXmlParser? bibleXmlParser}) {
_instance._bibleXmlParser = bibleXmlParser ?? BibleXmlParser();
return _instance;
}
ScriptureDatabase._internal();
late BibleXmlParser _bibleXmlParser;
late Box<Scripture> _scriptureBox;
// Mapping of BibleTranslation to its XML asset path
final Map<BibleTranslation, String> _translationFileMapping = {
BibleTranslation.esv: 'bible_xml/ESV.xml',
BibleTranslation.niv: 'bible_xml/NIV.xml',
BibleTranslation.nkjv: 'bible_xml/NKJV.xml',
BibleTranslation.nlt: 'bible_xml/NLT.xml',
BibleTranslation.nasb: 'bible_xml/NASB.xml',
BibleTranslation.kjv: 'bible_xml/KJV.xml',
BibleTranslation.msg: 'bible_xml/MSG.xml',
};
List<Scripture> _menstrualScriptures = [];
List<Scripture> _follicularScriptures = [];
List<Scripture> _ovulationScriptures = [];
List<Scripture> _lutealScriptures = [];
List<Scripture> _husbandScriptures = [];
List<Scripture> _womanhoodScriptures = [];
Map<String, List<Scripture>> _contextualScriptures = {};
// Hardcoded scriptures to ensure rich Husband experience immediately
final List<Scripture> _hardcodedHusbandScriptures = [
Scripture(
reference: "Mark 10:45",
verses: {
BibleTranslation.esv: "For even the Son of Man came not to be served but to serve, and to give his life as a ransom for many.",
BibleTranslation.niv: "For even the Son of Man did not come to be served, but to serve, and to give his life as a ransom for many.",
},
reflection: "True leadership is servanthood. How can you serve your wife today?",
applicablePhases: ['husband'],
applicableContexts: ['leadership', 'servant'],
),
Scripture(
reference: "Philippians 2:3-4",
verses: {
BibleTranslation.esv: "Do nothing from selfish ambition or conceit, but in humility count others more significant than yourselves. Let each of you look not only to his own interests, but also to the interests of others.",
},
reflection: "Humility is the foundation of a happy marriage.",
applicablePhases: ['husband'],
applicableContexts: ['servant', 'humility'],
),
Scripture(
reference: "Proverbs 29:18",
verses: {
BibleTranslation.esv: "Where there is no prophetic vision the people cast off restraint, but blessed is he who keeps the law.",
BibleTranslation.kjv: "Where there is no vision, the people perish: but he that keepeth the law, happy is he.",
},
reflection: "Lead your family with a clear, Godly vision.",
applicablePhases: ['husband'],
applicableContexts: ['vision', 'leadership'],
),
Scripture(
reference: "James 1:5",
verses: {
BibleTranslation.esv: "If any of you lacks wisdom, let him ask God, who gives generously to all without reproach, and it will be given him.",
},
reflection: "Seek God's wisdom in every decision you make for your family.",
applicablePhases: ['husband'],
applicableContexts: ['wisdom', 'vision'],
),
Scripture(
reference: "1 Timothy 3:4-5",
verses: {
BibleTranslation.esv: "He must manage his own household well, with all dignity keeping his children submissive, for if someone does not know how to manage his own household, how will he care for God's church?",
},
reflection: "Your first ministry is your home. Manage it with love and dignity.",
applicablePhases: ['husband'],
applicableContexts: ['leadership'],
),
Scripture(
reference: "Colossians 3:19",
verses: {
BibleTranslation.esv: "Husbands, love your wives, and do not be harsh with them.",
BibleTranslation.niv: "Husbands, love your wives and do not be harsh with them.",
},
reflection: "Gentleness is a sign of strength, not weakness.",
applicablePhases: ['husband'],
applicableContexts: ['kindness', 'love'],
),
Scripture(
reference: "1 Corinthians 16:14",
verses: {
BibleTranslation.esv: "Let all that you do be done in love.",
},
reflection: "Let love be the motivation behind every action and word.",
applicablePhases: ['husband'],
applicableContexts: ['love'],
),
];
Future<void> loadScriptures() async {
_scriptureBox = await Hive.openBox<Scripture>('scriptures');
if (_scriptureBox.isEmpty) {
print('Hive box is empty. Importing scriptures from optimized JSON data...');
// Load the pre-processed JSON file which already contains all verse text
final String response = await rootBundle.loadString('assets/scriptures_optimized.json');
final Map<String, dynamic> data = json.decode(response);
List<Scripture> importedScriptures = [];
// Helper function to process ANY list of scriptures
void processList(List<dynamic> list, String listName) {
for (var jsonEntry in list) {
final reference = jsonEntry['reference'];
final reflection = jsonEntry['reflection']; // Optional
final applicablePhases = (jsonEntry['applicablePhases'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ?? [];
final applicableContexts = (jsonEntry['applicableContexts'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ?? [];
// Map string keys (esv, niv) to BibleTranslation enum
Map<BibleTranslation, String> versesMap = {};
if (jsonEntry['verses'] != null) {
(jsonEntry['verses'] as Map<String, dynamic>).forEach((key, value) {
// Find enum by name (case-insensitive usually, but here keys are lowercase 'esv')
try {
final translation = BibleTranslation.values.firstWhere(
(e) => e.name.toLowerCase() == key.toLowerCase()
);
versesMap[translation] = value.toString();
} catch (e) {
print('Warning: Unknown translation key "$key" in optimized JSON');
}
});
}
if (versesMap.isNotEmpty) {
importedScriptures.add(Scripture(
verses: versesMap,
reference: reference,
reflection: reflection,
applicablePhases: applicablePhases,
applicableContexts: applicableContexts,
));
}
}
}
// Process all sections
if (data['menstrual'] != null) processList(data['menstrual'], 'menstrual');
if (data['follicular'] != null) processList(data['follicular'], 'follicular');
if (data['ovulation'] != null) processList(data['ovulation'], 'ovulation');
if (data['luteal'] != null) processList(data['luteal'], 'luteal');
if (data['husband'] != null) processList(data['husband'], 'husband');
if (data['womanhood'] != null) processList(data['womanhood'], 'womanhood');
if (data['contextual'] != null) {
final contextualMap = data['contextual'] as Map<String, dynamic>;
contextualMap.forEach((key, value) {
processList(value as List, 'contextual_$key');
});
}
// Store all imported scriptures into Hive
for (var scripture in importedScriptures) {
await _scriptureBox.put(scripture.reference, scripture); // Using reference as key
}
} else {
print('Hive box is not empty. Loading scriptures from Hive...');
}
// Populate internal lists from Hive box values
_menstrualScriptures = _scriptureBox.values
.where((s) => s.applicablePhases.contains('menstrual'))
.toList();
_follicularScriptures = _scriptureBox.values
.where((s) => s.applicablePhases.contains('follicular'))
.toList();
_ovulationScriptures = _scriptureBox.values
.where((s) => s.applicablePhases.contains('ovulation'))
.toList();
_lutealScriptures = _scriptureBox.values
.where((s) => s.applicablePhases.contains('luteal'))
.toList();
_husbandScriptures = [
..._scriptureBox.values.where((s) => s.applicablePhases.contains('husband')),
..._hardcodedHusbandScriptures,
];
// Remove duplicates based on reference if any
final uniqueHusbandIds = <String>{};
_husbandScriptures = _husbandScriptures.where((s) {
if (uniqueHusbandIds.contains(s.reference)) return false;
uniqueHusbandIds.add(s.reference);
return true;
}).toList();
_womanhoodScriptures = _scriptureBox.values
.where((s) => s.applicableContexts.contains('womanhood'))
.toList();
_contextualScriptures = {
'anxiety': _scriptureBox.values.where((s) => s.applicableContexts.contains('anxiety')).toList(),
'pain': _scriptureBox.values.where((s) => s.applicableContexts.contains('pain')).toList(),
'fatigue': _scriptureBox.values.where((s) => s.applicableContexts.contains('fatigue')).toList(),
'joy': _scriptureBox.values.where((s) => s.applicableContexts.contains('joy')).toList(),
};
}
/// Get the number of scriptures for a given phase
int getScriptureCountForPhase(String phase) {
switch (phase.toLowerCase()) {
case 'menstrual':
return _menstrualScriptures.length;
case 'follicular':
return _follicularScriptures.length;
case 'ovulation':
return _ovulationScriptures.length;
case 'luteal':
return _lutealScriptures.length;
case 'husband':
return _husbandScriptures.length;
case 'womanhood':
return _womanhoodScriptures.length;
case 'anxiety':
return _contextualScriptures['anxiety']?.length ?? 0;
case 'pain':
return _contextualScriptures['pain']?.length ?? 0;
case 'fatigue':
return _contextualScriptures['fatigue']?.length ?? 0;
case 'joy':
return _contextualScriptures['joy']?.length ?? 0;
default:
return 0;
}
}
/// Get recommended scripture based on entry
static Scripture? getRecommendedScripture(CycleEntry entry) {
if (entry.mood == MoodLevel.verySad || entry.mood == MoodLevel.sad || (entry.stressLevel != null && entry.stressLevel! > 3)) {
return contextualScriptures['anxiety']![DateTime.now().day % contextualScriptures['anxiety']!.length];
Scripture? getRecommendedScripture(CycleEntry entry) {
if (entry.mood == MoodLevel.verySad ||
entry.mood == MoodLevel.sad ||
(entry.stressLevel != null && entry.stressLevel! > 3)) {
final scriptures = _contextualScriptures['anxiety'];
if (scriptures != null && scriptures.isNotEmpty) {
return scriptures[DateTime.now().day % scriptures.length];
}
}
if ((entry.crampIntensity != null && entry.crampIntensity! >= 3) || entry.hasHeadache || entry.hasLowerBackPain) {
return contextualScriptures['pain']![DateTime.now().day % contextualScriptures['pain']!.length];
if ((entry.crampIntensity != null && entry.crampIntensity! >= 3) ||
entry.hasHeadache ||
entry.hasLowerBackPain) {
final scriptures = _contextualScriptures['pain'];
if (scriptures != null && scriptures.isNotEmpty) {
return scriptures[DateTime.now().day % scriptures.length];
}
}
if (entry.hasFatigue || entry.hasInsomnia || (entry.energyLevel != null && entry.energyLevel! <= 2)) {
return contextualScriptures['fatigue']![DateTime.now().day % contextualScriptures['fatigue']!.length];
if (entry.hasFatigue ||
entry.hasInsomnia ||
(entry.energyLevel != null && entry.energyLevel! <= 2)) {
final scriptures = _contextualScriptures['fatigue'];
if (scriptures != null && scriptures.isNotEmpty) {
return scriptures[DateTime.now().day % scriptures.length];
}
}
if (entry.mood == MoodLevel.veryHappy) {
return contextualScriptures['joy']![DateTime.now().day % contextualScriptures['joy']!.length];
final scriptures = _contextualScriptures['joy'];
if (scriptures != null && scriptures.isNotEmpty) {
return scriptures[DateTime.now().day % scriptures.length];
}
}
return null;
}
>>>>>>> 6742220 (Your commit message here)
/// Get scripture for current phase
static Scripture getScriptureForPhase(String phase) {
final List<Scripture> scriptures;
/// Get scripture for current phase by index
Scripture? getScriptureForPhaseByIndex(String phase, int index) {
List<Scripture> scriptures;
switch (phase.toLowerCase()) {
case 'menstrual':
scriptures = menstrualScriptures;
scriptures = _menstrualScriptures;
break;
case 'follicular':
scriptures = follicularScriptures;
scriptures = _follicularScriptures;
break;
case 'ovulation':
scriptures = ovulationScriptures;
scriptures = _ovulationScriptures;
break;
case 'luteal':
scriptures = lutealScriptures;
scriptures = _lutealScriptures;
break;
case 'husband':
scriptures = _husbandScriptures;
break;
case 'womanhood':
scriptures = _womanhoodScriptures;
break;
case 'anxiety':
scriptures = _contextualScriptures['anxiety'] ?? [];
break;
case 'pain':
scriptures = _contextualScriptures['pain'] ?? [];
break;
case 'fatigue':
scriptures = _contextualScriptures['fatigue'] ?? [];
break;
case 'joy':
scriptures = _contextualScriptures['joy'] ?? [];
break;
default:
scriptures = [...menstrualScriptures, ...follicularScriptures, ...ovulationScriptures, ...lutealScriptures];
return null;
}
// Return a scripture based on the day of year for variety
final dayOfYear = DateTime.now().difference(DateTime(DateTime.now().year, 1, 1)).inDays;
return scriptures[dayOfYear % scriptures.length];
if (scriptures.isEmpty || index < 0 || index >= scriptures.length) {
return null;
}
return scriptures[index];
}
/// Get scripture for husband
static Scripture getHusbandScripture() {
final dayOfYear = DateTime.now().difference(DateTime(DateTime.now().year, 1, 1)).inDays;
return husbandScriptures[dayOfYear % husbandScriptures.length];
// ... imports
// ... inside ScriptureDatabase class
/// Get a random scripture for a given phase
Scripture? getRandomScriptureForPhase(String phase) {
List<Scripture> scriptures;
switch (phase.toLowerCase()) {
case 'menstrual':
scriptures = _menstrualScriptures;
break;
case 'follicular':
scriptures = _follicularScriptures;
break;
case 'ovulation':
scriptures = _ovulationScriptures;
break;
case 'luteal':
scriptures = _lutealScriptures;
break;
case 'husband':
scriptures = _husbandScriptures;
break;
case 'womanhood':
scriptures = _womanhoodScriptures;
break;
case 'anxiety':
scriptures = _contextualScriptures['anxiety'] ?? [];
break;
case 'pain':
scriptures = _contextualScriptures['pain'] ?? [];
break;
case 'fatigue':
scriptures = _contextualScriptures['fatigue'] ?? [];
break;
case 'joy':
scriptures = _contextualScriptures['joy'] ?? [];
break;
default:
return null;
}
if (scriptures.isEmpty) {
return null;
}
return scriptures[Random().nextInt(scriptures.length)];
}
/// Get scripture for current phase (Randomized)
Scripture getScriptureForPhase(String phase) {
List<Scripture> scriptures;
switch (phase.toLowerCase()) {
// ... (same switch cases)
case 'menstrual':
scriptures = _menstrualScriptures;
break;
case 'follicular':
scriptures = _follicularScriptures;
break;
case 'ovulation':
scriptures = _ovulationScriptures;
break;
case 'luteal':
scriptures = _lutealScriptures;
break;
case 'husband':
scriptures = _husbandScriptures;
break;
case 'womanhood':
scriptures = _womanhoodScriptures;
break;
case 'anxiety':
scriptures = _contextualScriptures['anxiety'] ?? [];
break;
case 'pain':
scriptures = _contextualScriptures['pain'] ?? [];
break;
case 'fatigue':
scriptures = _contextualScriptures['fatigue'] ?? [];
break;
case 'joy':
scriptures = _contextualScriptures['joy'] ?? [];
break;
default:
// Fallback
scriptures = [
..._menstrualScriptures,
..._follicularScriptures,
..._ovulationScriptures,
..._lutealScriptures,
..._husbandScriptures,
..._womanhoodScriptures,
...(_contextualScriptures['anxiety'] ?? []),
...(_contextualScriptures['pain'] ?? []),
...(_contextualScriptures['fatigue'] ?? []),
...(_contextualScriptures['joy'] ?? []),
];
if (scriptures.isEmpty) return Scripture(verses: {BibleTranslation.esv: "No scripture found."}, reference: "Unknown", applicablePhases: [], applicableContexts: []);
}
if (scriptures.isEmpty) return Scripture(verses: {BibleTranslation.esv: "No scripture found."}, reference: "Unknown", applicablePhases: [], applicableContexts: []);
return scriptures[Random().nextInt(scriptures.length)];
}
/// Get scripture for husband (Randomized)
Scripture getHusbandScripture() {
final scriptures = _husbandScriptures;
if (scriptures.isEmpty) {
return Scripture(verses: {BibleTranslation.esv: "No husband scripture found."}, reference: "Unknown", applicablePhases: [], applicableContexts: []);
}
return scriptures[Random().nextInt(scriptures.length)];
}
/// Get all scriptures
static List<Scripture> getAllScriptures() {
return [
...menstrualScriptures,
...follicularScriptures,
...ovulationScriptures,
...lutealScriptures,
...womanhoodScriptures,
];
List<Scripture> getAllScriptures() {
return _scriptureBox.values.toList();
}
}

View File

@@ -0,0 +1,53 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'scripture.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ScriptureAdapter extends TypeAdapter<Scripture> {
@override
final int typeId = 10;
@override
Scripture read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Scripture(
verses: (fields[0] as Map).cast<BibleTranslation, String>(),
reference: fields[1] as String,
reflection: fields[2] as String?,
applicablePhases: (fields[3] as List).cast<String>(),
applicableContexts: (fields[4] as List).cast<String>(),
);
}
@override
void write(BinaryWriter writer, Scripture obj) {
writer
..writeByte(5)
..writeByte(0)
..write(obj.verses)
..writeByte(1)
..write(obj.reference)
..writeByte(2)
..write(obj.reflection)
..writeByte(3)
..write(obj.applicablePhases)
..writeByte(4)
..write(obj.applicableContexts);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ScriptureAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -7,10 +7,10 @@ part 'user_profile.g.dart';
enum RelationshipStatus {
@HiveField(0)
single,
@HiveField(1)
engaged,
@HiveField(2)
married,
}
@@ -20,16 +20,14 @@ enum RelationshipStatus {
enum FertilityGoal {
@HiveField(0)
tryingToConceive, // TTC
@HiveField(1)
tryingToAvoid, // TTA - NFP
@HiveField(2)
justTracking,
}
<<<<<<< HEAD
=======
@HiveType(typeId: 9)
enum BibleTranslation {
@HiveField(0)
@@ -44,63 +42,67 @@ enum BibleTranslation {
nasb,
@HiveField(5)
kjv,
@HiveField(6)
msg,
}
>>>>>>> 6742220 (Your commit message here)
/// User profile model
@HiveType(typeId: 2)
class UserProfile extends HiveObject {
@HiveField(0)
String id;
@HiveField(1)
String name;
@HiveField(2)
RelationshipStatus relationshipStatus;
@HiveField(3)
FertilityGoal? fertilityGoal;
@HiveField(4)
int averageCycleLength;
@HiveField(5)
int averagePeriodLength;
@HiveField(6)
DateTime? lastPeriodStartDate;
@HiveField(7)
bool notificationsEnabled;
@HiveField(8)
String? devotionalTime; // HH:mm format
@HiveField(9)
bool hasCompletedOnboarding;
@HiveField(10)
DateTime createdAt;
@HiveField(11)
DateTime updatedAt;
@HiveField(12)
String? partnerName; // For married users
@HiveField(14, defaultValue: UserRole.wife)
UserRole role;
@HiveField(15, defaultValue: false)
bool isIrregularCycle;
<<<<<<< HEAD
=======
@HiveField(16, defaultValue: BibleTranslation.esv)
BibleTranslation bibleTranslation;
>>>>>>> 6742220 (Your commit message here)
@HiveField(17)
List<String>? favoriteFoods;
@HiveField(18, defaultValue: false)
bool isDataShared;
UserProfile({
required this.id,
required this.name,
@@ -115,36 +117,35 @@ class UserProfile extends HiveObject {
required this.createdAt,
required this.updatedAt,
this.partnerName,
<<<<<<< HEAD
this.role = UserRole.wife,
this.isIrregularCycle = false,
=======
this.role = UserRole.wife,
this.isIrregularCycle = false,
this.bibleTranslation = BibleTranslation.esv,
>>>>>>> 6742220 (Your commit message here)
this.favoriteFoods,
this.isDataShared = false,
});
/// Check if user is married
bool get isMarried => relationshipStatus == RelationshipStatus.married;
/// Check if user is trying to conceive
bool get isTTC => fertilityGoal == FertilityGoal.tryingToConceive;
/// Check if user is practicing NFP
bool get isNFP => fertilityGoal == FertilityGoal.tryingToAvoid;
/// Check if user is husband
bool get isHusband => role == UserRole.husband;
/// Should show fertility content
bool get showFertilityContent =>
!isHusband && isMarried && fertilityGoal != FertilityGoal.justTracking && fertilityGoal != null;
bool get showFertilityContent =>
!isHusband &&
isMarried &&
fertilityGoal != FertilityGoal.justTracking &&
fertilityGoal != null;
/// Should show intimacy recommendations
bool get showIntimacyContent => isMarried;
/// Copy with updated fields
UserProfile copyWith({
String? id,
@@ -160,15 +161,11 @@ class UserProfile extends HiveObject {
DateTime? createdAt,
DateTime? updatedAt,
String? partnerName,
<<<<<<< HEAD
UserRole? role,
bool? isIrregularCycle,
=======
UserRole? role,
bool? isIrregularCycle,
BibleTranslation? bibleTranslation,
>>>>>>> 6742220 (Your commit message here)
List<String>? favoriteFoods,
bool? isDataShared,
}) {
return UserProfile(
id: id ?? this.id,
@@ -180,39 +177,41 @@ class UserProfile extends HiveObject {
lastPeriodStartDate: lastPeriodStartDate ?? this.lastPeriodStartDate,
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
devotionalTime: devotionalTime ?? this.devotionalTime,
hasCompletedOnboarding: hasCompletedOnboarding ?? this.hasCompletedOnboarding,
hasCompletedOnboarding:
hasCompletedOnboarding ?? this.hasCompletedOnboarding,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? DateTime.now(),
partnerName: partnerName ?? this.partnerName,
<<<<<<< HEAD
role: role ?? this.role,
isIrregularCycle: isIrregularCycle ?? this.isIrregularCycle,
=======
role: role ?? this.role,
isIrregularCycle: isIrregularCycle ?? this.isIrregularCycle,
bibleTranslation: bibleTranslation ?? this.bibleTranslation,
>>>>>>> 6742220 (Your commit message here)
favoriteFoods: favoriteFoods ?? this.favoriteFoods,
isDataShared: isDataShared ?? this.isDataShared,
);
}
}
<<<<<<< HEAD
=======
extension BibleTranslationExtension on BibleTranslation {
String get label {
switch (this) {
case BibleTranslation.esv: return 'ESV';
case BibleTranslation.niv: return 'NIV';
case BibleTranslation.nkjv: return 'NKJV';
case BibleTranslation.nlt: return 'NLT';
case BibleTranslation.nasb: return 'NASB';
case BibleTranslation.kjv: return 'KJV';
case BibleTranslation.esv:
return 'ESV';
case BibleTranslation.niv:
return 'NIV';
case BibleTranslation.nkjv:
return 'NKJV';
case BibleTranslation.nlt:
return 'NLT';
case BibleTranslation.nasb:
return 'NASB';
case BibleTranslation.kjv:
return 'KJV';
case BibleTranslation.msg:
return 'MSG';
}
}
}
>>>>>>> 6742220 (Your commit message here)
@HiveType(typeId: 8)
enum UserRole {
@HiveField(0)

View File

@@ -32,23 +32,18 @@ class UserProfileAdapter extends TypeAdapter<UserProfile> {
partnerName: fields[12] as String?,
role: fields[14] == null ? UserRole.wife : fields[14] as UserRole,
isIrregularCycle: fields[15] == null ? false : fields[15] as bool,
<<<<<<< HEAD
=======
bibleTranslation: fields[16] == null
? BibleTranslation.esv
: fields[16] as BibleTranslation,
>>>>>>> 6742220 (Your commit message here)
favoriteFoods: (fields[17] as List?)?.cast<String>(),
isDataShared: fields[18] == null ? false : fields[18] as bool,
);
}
@override
void write(BinaryWriter writer, UserProfile obj) {
writer
<<<<<<< HEAD
..writeByte(15)
=======
..writeByte(16)
>>>>>>> 6742220 (Your commit message here)
..writeByte(18)
..writeByte(0)
..write(obj.id)
..writeByte(1)
@@ -78,13 +73,13 @@ class UserProfileAdapter extends TypeAdapter<UserProfile> {
..writeByte(14)
..write(obj.role)
..writeByte(15)
<<<<<<< HEAD
..write(obj.isIrregularCycle);
=======
..write(obj.isIrregularCycle)
..writeByte(16)
..write(obj.bibleTranslation);
>>>>>>> 6742220 (Your commit message here)
..write(obj.bibleTranslation)
..writeByte(17)
..write(obj.favoriteFoods)
..writeByte(18)
..write(obj.isDataShared);
}
@override
@@ -186,8 +181,6 @@ class FertilityGoalAdapter extends TypeAdapter<FertilityGoal> {
typeId == other.typeId;
}
<<<<<<< HEAD
=======
class BibleTranslationAdapter extends TypeAdapter<BibleTranslation> {
@override
final int typeId = 9;
@@ -207,6 +200,8 @@ class BibleTranslationAdapter extends TypeAdapter<BibleTranslation> {
return BibleTranslation.nasb;
case 5:
return BibleTranslation.kjv;
case 6:
return BibleTranslation.msg;
default:
return BibleTranslation.esv;
}
@@ -233,6 +228,9 @@ class BibleTranslationAdapter extends TypeAdapter<BibleTranslation> {
case BibleTranslation.kjv:
writer.writeByte(5);
break;
case BibleTranslation.msg:
writer.writeByte(6);
break;
}
}
@@ -247,7 +245,6 @@ class BibleTranslationAdapter extends TypeAdapter<BibleTranslation> {
typeId == other.typeId;
}
>>>>>>> 6742220 (Your commit message here)
class UserRoleAdapter extends TypeAdapter<UserRole> {
@override
final int typeId = 8;

View File

@@ -0,0 +1,113 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/scripture.dart';
import '../models/cycle_entry.dart';
import 'user_provider.dart';
import 'package:collection/collection.dart'; // For IterableExtension
// State for ScriptureProvider
class ScriptureState {
final Scripture? currentScripture;
final CyclePhase? currentPhase;
final int currentIndex; // Index within the phase-specific list
final int? maxIndex; // Max number of scriptures for the current phase
ScriptureState({
this.currentScripture,
this.currentPhase,
this.currentIndex = 0,
this.maxIndex,
});
ScriptureState copyWith({
Scripture? currentScripture,
CyclePhase? currentPhase,
int? currentIndex,
int? maxIndex,
}) {
return ScriptureState(
currentScripture: currentScripture ?? this.currentScripture,
currentPhase: currentPhase ?? this.currentPhase,
currentIndex: currentIndex ?? this.currentIndex,
maxIndex: maxIndex ?? this.maxIndex,
);
}
}
// StateNotifier for ScriptureProvider
class ScriptureNotifier extends StateNotifier<ScriptureState> {
final ScriptureDatabase _scriptureDatabase;
final Ref _ref;
ScriptureNotifier(this._scriptureDatabase, this._ref) : super(ScriptureState()) {
// We don't initialize here directly, as we need the phase from other providers.
// Initialization will be triggered by the UI.
}
// Initialize/refresh scripture for a given phase
// This should be called by the consuming widget when the phase changes or on initial load.
Future<void> initializeScripture(CyclePhase phase) async {
// Only re-initialize if the phase has changed or no scripture is currently set
if (state.currentPhase != phase || state.currentScripture == null) {
final scriptureCount = _scriptureDatabase.getScriptureCountForPhase(phase.name);
if (scriptureCount > 0) {
// Use day of year to get a stable initial scripture for the day
final dayOfYear =
DateTime.now().difference(DateTime(DateTime.now().year, 1, 1)).inDays;
final initialIndex = dayOfYear % scriptureCount;
state = state.copyWith(
currentPhase: phase,
currentIndex: initialIndex,
maxIndex: scriptureCount,
currentScripture: _scriptureDatabase.getScriptureForPhaseByIndex(
phase.name, initialIndex),
);
} else {
state = state.copyWith(
currentPhase: phase,
currentScripture: null,
currentIndex: 0,
maxIndex: 0,
);
}
}
}
void getNextScripture() {
if (state.currentPhase == null || state.maxIndex == null || state.maxIndex == 0) return;
final nextIndex = (state.currentIndex + 1) % state.maxIndex!;
_updateScripture(nextIndex);
}
void getPreviousScripture() {
if (state.currentPhase == null || state.maxIndex == null || state.maxIndex == 0) return;
final prevIndex = (state.currentIndex - 1 + state.maxIndex!) % state.maxIndex!;
_updateScripture(prevIndex);
}
void getRandomScripture() {
if (state.currentPhase == null || state.maxIndex == null || state.maxIndex == 0) return;
// Use a proper random number generator for better randomness
final randomIndex = DateTime.now().microsecondsSinceEpoch % state.maxIndex!; // Still using timestamp for simplicity
_updateScripture(randomIndex);
}
void _updateScripture(int newIndex) {
if (state.currentPhase == null) return;
final newScripture = _scriptureDatabase.getScriptureForPhaseByIndex(
state.currentPhase!.name, newIndex);
state = state.copyWith(
currentIndex: newIndex,
currentScripture: newScripture,
);
}
}
final scriptureDatabaseProvider = Provider((ref) => ScriptureDatabase());
final scriptureProvider =
StateNotifierProvider<ScriptureNotifier, ScriptureState>((ref) {
return ScriptureNotifier(ref.watch(scriptureDatabaseProvider), ref);
});

View File

@@ -55,6 +55,12 @@ class CycleEntriesNotifier extends StateNotifier<List<CycleEntry>> {
_loadEntries();
}
Future<void> updateEntry(CycleEntry entry) async {
final box = Hive.box<CycleEntry>('cycle_entries');
await box.put(entry.id, entry);
_loadEntries();
}
Future<void> deleteEntry(String id) async {
final box = Hive.box<CycleEntry>('cycle_entries');
await box.delete(id);

View File

@@ -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];
}

View File

@@ -6,18 +6,31 @@ import '../../providers/user_provider.dart';
import '../../services/cycle_service.dart';
import '../../models/cycle_entry.dart';
import '../../theme/app_theme.dart';
<<<<<<< HEAD
=======
import '../../widgets/scripture_card.dart';
import '../../models/user_profile.dart';
>>>>>>> 6742220 (Your commit message here)
import '../../providers/scripture_provider.dart'; // Import the new provider
class DevotionalScreen extends ConsumerWidget {
class DevotionalScreen extends ConsumerStatefulWidget {
const DevotionalScreen({super.key});
<<<<<<< HEAD
=======
Future<void> _showTranslationPicker(BuildContext context, WidgetRef ref, UserProfile? user) async {
@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>(
@@ -38,32 +51,46 @@ class DevotionalScreen extends ConsumerWidget {
),
),
...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),
)),
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)
);
await ref
.read(userProfileProvider.notifier)
.updateProfile(user.copyWith(bibleTranslation: selected));
}
}
>>>>>>> 6742220 (Your commit message here)
@override
Widget build(BuildContext context, WidgetRef ref) {
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'] as CyclePhase;
final scripture = ScriptureDatabase.getScriptureForPhase(phase.name);
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(
@@ -85,7 +112,8 @@ class DevotionalScreen extends ConsumerWidget {
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
@@ -117,75 +145,53 @@ class DevotionalScreen extends ConsumerWidget {
),
const SizedBox(height: 32),
// Main Scripture Card
<<<<<<< HEAD
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
_getPhaseColor(phase).withOpacity(0.15),
AppColors.cream,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
// 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),
),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: _getPhaseColor(phase).withOpacity(0.3),
),
),
child: Column(
children: [
// Quote icon
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.format_quote,
color: _getPhaseColor(phase),
size: 24,
),
),
const SizedBox(height: 20),
// Verse
Text(
'"${scripture.verse}"',
textAlign: TextAlign.center,
style: GoogleFonts.lora(
fontSize: 20,
fontStyle: FontStyle.italic,
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,
height: 1.6,
),
),
const SizedBox(height: 16),
// Reference
Text(
'${scripture.reference}',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.warmGray,
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,
),
),
=======
ScriptureCard(
verse: scripture.getVerse(user?.bibleTranslation ?? BibleTranslation.esv),
reference: scripture.reference,
translation: (user?.bibleTranslation ?? BibleTranslation.esv).label,
phase: phase,
onTranslationTap: () => _showTranslationPicker(context, ref, user),
>>>>>>> 6742220 (Your commit message here)
),
const SizedBox(height: 24),

View File

@@ -1,10 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
<<<<<<< HEAD
import 'package:hive_flutter/hive_flutter.dart';
=======
>>>>>>> 6742220 (Your commit message here)
import '../../theme/app_theme.dart';
import '../../models/user_profile.dart';
import '../../models/cycle_entry.dart';
@@ -17,28 +13,10 @@ import '../../widgets/cycle_ring.dart';
import '../../widgets/scripture_card.dart';
import '../../widgets/quick_log_buttons.dart';
import '../../providers/user_provider.dart';
<<<<<<< HEAD
import '../../services/cycle_service.dart';
class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({super.key});
@override
ConsumerState<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends ConsumerState<HomeScreen> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _selectedIndex,
=======
import '../../providers/navigation_provider.dart';
import '../../services/cycle_service.dart';
import '../../services/bible_utils.dart';
import '../../providers/scripture_provider.dart'; // Import the new provider
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@@ -50,45 +28,34 @@ class HomeScreen extends ConsumerWidget {
return Scaffold(
body: IndexedStack(
index: selectedIndex,
>>>>>>> 6742220 (Your commit message here)
children: [
const _DashboardTab(),
const CalendarScreen(),
const LogScreen(),
const DevotionalScreen(),
<<<<<<< HEAD
_SettingsTab(onReset: () => setState(() => _selectedIndex = 0)),
=======
_SettingsTab(onReset: () => ref.read(navigationProvider.notifier).setIndex(0)),
>>>>>>> 6742220 (Your commit message here)
_SettingsTab(
onReset: () =>
ref.read(navigationProvider.notifier).setIndex(0)),
],
),
bottomNavigationBar: Container(
decoration: BoxDecoration(
<<<<<<< HEAD
color: Colors.white,
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.1),
=======
color: Theme.of(context).bottomNavigationBarTheme.backgroundColor,
boxShadow: [
BoxShadow(
color: (Theme.of(context).brightness == Brightness.dark ? Colors.black : AppColors.charcoal).withOpacity(0.1),
>>>>>>> 6742220 (Your commit message here)
color: (Theme.of(context).brightness == Brightness.dark
? Colors.black
: AppColors.charcoal)
.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: BottomNavigationBar(
<<<<<<< HEAD
currentIndex: _selectedIndex,
onTap: (index) => setState(() => _selectedIndex = index),
=======
currentIndex: selectedIndex,
onTap: (index) => ref.read(navigationProvider.notifier).setIndex(index),
>>>>>>> 6742220 (Your commit message here)
onTap: (index) =>
ref.read(navigationProvider.notifier).setIndex(index),
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
@@ -122,34 +89,61 @@ class HomeScreen extends ConsumerWidget {
}
}
class _DashboardTab extends ConsumerWidget {
const _DashboardTab();
class _DashboardTab extends ConsumerStatefulWidget {
const _DashboardTab({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
<<<<<<< HEAD
final user = ref.watch(userProfileProvider);
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 name = user?.name ?? 'Friend';
final phase = cycleInfo['phase'] as CyclePhase;
final dayOfCycle = cycleInfo['dayOfCycle'] as int;
final cycleLength = user?.averageCycleLength ?? 28;
// Get scripture for current phase
=======
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'] as CyclePhase;
final dayOfCycle = cycleInfo['dayOfCycle'] as int;
>>>>>>> 6742220 (Your commit message here)
final scripture = ScriptureDatabase.getScriptureForPhase(phase.name);
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(
@@ -157,40 +151,8 @@ class _DashboardTab extends ConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
<<<<<<< HEAD
// Greeting
_buildGreeting(name),
const SizedBox(height: 24),
// Cycle Ring
Center(
child: CycleRing(
dayOfCycle: dayOfCycle,
totalDays: cycleLength,
phase: phase,
),
),
const SizedBox(height: 24),
// Scripture Card
ScriptureCard(
verse: scripture.verse,
reference: scripture.reference,
phase: phase,
),
const SizedBox(height: 20),
// Quick Log Buttons
Text(
'Quick Log',
style: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
=======
_buildGreeting(context, name),
const SizedBox(height: 24),
Center(
child: CycleRing(
dayOfCycle: dayOfCycle,
@@ -199,38 +161,65 @@ class _DashboardTab extends ConsumerWidget {
),
),
const SizedBox(height: 32),
ScriptureCard(
verse: scripture.getVerse(translation),
reference: scripture.reference,
translation: translation.label,
phase: phase,
onTranslationTap: () => BibleUtils.showTranslationPicker(context, ref),
// 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: AppColors.sageGreen,
),
),
),
const SizedBox(height: 24),
Text(
'Quick Log',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 18,
fontWeight: FontWeight.w600,
>>>>>>> 6742220 (Your commit message here)
),
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
const QuickLogButtons(),
<<<<<<< HEAD
const SizedBox(height: 20),
// Today's Tip - Only show if not just tracking or husband (though husband has own screen)
if (user?.role == UserRole.wife)
TipCard(phase: phase, isMarried: user?.isMarried ?? false),
=======
const SizedBox(height: 24),
if (role == UserRole.wife)
TipCard(phase: phase, isMarried: isMarried),
>>>>>>> 6742220 (Your commit message here)
const SizedBox(height: 20),
],
),
@@ -238,12 +227,8 @@ class _DashboardTab extends ConsumerWidget {
);
}
<<<<<<< HEAD
Widget _buildGreeting(String name) {
=======
Widget _buildGreeting(BuildContext context, String name) {
final theme = Theme.of(context);
>>>>>>> 6742220 (Your commit message here)
final hour = DateTime.now().hour;
String greeting;
if (hour < 12) {
@@ -264,26 +249,15 @@ class _DashboardTab extends ConsumerWidget {
'$greeting,',
style: GoogleFonts.outfit(
fontSize: 16,
<<<<<<< HEAD
color: AppColors.warmGray,
=======
color: theme.colorScheme.onSurfaceVariant,
>>>>>>> 6742220 (Your commit message here)
),
),
Text(
name,
<<<<<<< HEAD
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
=======
style: theme.textTheme.displaySmall?.copyWith(
fontSize: 28,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
>>>>>>> 6742220 (Your commit message here)
),
),
],
@@ -293,64 +267,41 @@ class _DashboardTab extends ConsumerWidget {
width: 48,
height: 48,
decoration: BoxDecoration(
<<<<<<< HEAD
color: AppColors.blushPink,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.notifications_outlined,
color: AppColors.rose,
=======
color: theme.colorScheme.primaryContainer.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.notifications_outlined,
color: theme.colorScheme.primary,
>>>>>>> 6742220 (Your commit message here)
),
),
],
);
}
<<<<<<< HEAD
// Placeholder _calculateCycleInfo removed as it's now in CycleService
=======
>>>>>>> 6742220 (Your commit message here)
}
class _SettingsTab extends ConsumerWidget {
final VoidCallback? onReset;
const _SettingsTab({this.onReset});
Widget _buildSettingsTile(BuildContext context, IconData icon, String title, {VoidCallback? onTap}) {
Widget _buildSettingsTile(BuildContext context, IconData icon, String title,
{VoidCallback? onTap}) {
return ListTile(
<<<<<<< HEAD
leading: Icon(icon, color: AppColors.charcoal),
title: Text(
title,
style: GoogleFonts.outfit(
fontSize: 16,
color: AppColors.charcoal,
),
),
trailing: Icon(Icons.chevron_right, color: AppColors.lightGray),
=======
leading: Icon(icon, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8)),
leading: Icon(icon,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8)),
title: Text(
title,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontSize: 16,
),
fontSize: 16,
),
),
trailing: const Icon(Icons.chevron_right, color: AppColors.lightGray),
>>>>>>> 6742220 (Your commit message here)
onTap: onTap ?? () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Settings coming soon!')),
);
},
onTap: onTap ??
() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Settings coming soon!')),
);
},
);
}
@@ -359,11 +310,14 @@ class _SettingsTab extends ConsumerWidget {
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?'),
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),
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel')),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Reset', style: TextStyle(color: Colors.red)),
),
],
@@ -373,7 +327,7 @@ class _SettingsTab extends ConsumerWidget {
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);
@@ -383,14 +337,19 @@ class _SettingsTab extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
<<<<<<< HEAD
final user = ref.watch(userProfileProvider);
=======
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';
>>>>>>> 6742220 (Your commit message here)
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';
return SafeArea(
child: SingleChildScrollView(
@@ -400,43 +359,21 @@ class _SettingsTab extends ConsumerWidget {
children: [
Text(
'Settings',
<<<<<<< HEAD
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
=======
style: Theme.of(context).textTheme.displayMedium?.copyWith(
fontSize: 28,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
>>>>>>> 6742220 (Your commit message here)
),
fontSize: 28,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 24),
<<<<<<< HEAD
// Profile Card
Container(
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),
),
],
=======
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)),
>>>>>>> 6742220 (Your commit message here)
border: Border.all(
color:
Theme.of(context).colorScheme.outline.withOpacity(0.05)),
),
child: Row(
children: [
@@ -445,7 +382,10 @@ class _SettingsTab extends ConsumerWidget {
height: 60,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.blushPink, AppColors.rose.withOpacity(0.7)],
colors: [
AppColors.blushPink,
AppColors.rose.withOpacity(0.7)
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
@@ -453,11 +393,7 @@ class _SettingsTab extends ConsumerWidget {
),
child: Center(
child: Text(
<<<<<<< HEAD
user?.name.isNotEmpty == true ? user!.name[0].toUpperCase() : '?',
=======
name.isNotEmpty ? name[0].toUpperCase() : '?',
>>>>>>> 6742220 (Your commit message here)
style: GoogleFonts.outfit(
fontSize: 24,
fontWeight: FontWeight.w600,
@@ -472,28 +408,15 @@ class _SettingsTab extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
<<<<<<< HEAD
user?.name ?? 'Guest',
style: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
Text(
user?.role == UserRole.husband
? 'HUSBAND'
: (user?.relationshipStatus.name.toUpperCase() ?? 'SINGLE'),
=======
name,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 18,
fontWeight: FontWeight.w600,
),
style:
Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
Text(
roleSymbol ?? relationshipStatus,
>>>>>>> 6742220 (Your commit message here)
style: GoogleFonts.outfit(
fontSize: 12,
letterSpacing: 1,
@@ -503,56 +426,48 @@ class _SettingsTab extends ConsumerWidget {
],
),
),
<<<<<<< HEAD
Icon(Icons.chevron_right, color: AppColors.warmGray),
=======
const Icon(Icons.chevron_right, color: AppColors.warmGray),
>>>>>>> 6742220 (Your commit message here)
],
),
),
const SizedBox(height: 24),
<<<<<<< HEAD
// Settings Groups
_buildSettingsGroup('Preferences', [
_buildSettingsTile(context, Icons.notifications_outlined, 'Notifications'),
=======
_buildSettingsGroup(context, 'Preferences', [
_buildSettingsTile(context, Icons.notifications_outlined, 'Notifications'),
_buildSettingsTile(
context,
Icons.book_outlined,
context, Icons.notifications_outlined, 'Notifications'),
_buildSettingsTile(
context,
Icons.book_outlined,
'Bible Version ($translationLabel)',
onTap: () => BibleUtils.showTranslationPicker(context, ref),
),
>>>>>>> 6742220 (Your commit message here)
_buildSettingsTile(context, Icons.palette_outlined, 'Appearance'),
_buildSettingsTile(context, Icons.lock_outline, 'Privacy'),
]),
const SizedBox(height: 16),
<<<<<<< HEAD
_buildSettingsGroup('Cycle', [
=======
_buildSettingsGroup(context, 'Cycle', [
>>>>>>> 6742220 (Your commit message here)
_buildSettingsTile(context, Icons.calendar_today_outlined, 'Cycle Settings'),
_buildSettingsTile(context, Icons.trending_up_outlined, 'Cycle History'),
_buildSettingsTile(context, Icons.download_outlined, 'Export Data'),
]),
const SizedBox(height: 16),
<<<<<<< HEAD
_buildSettingsGroup('Account', [
=======
_buildSettingsGroup(context, 'Account', [
>>>>>>> 6742220 (Your commit message here)
_buildSettingsTile(
context,
Icons.logout,
'Reset App / Logout',
onTap: () => _resetApp(context, ref)
context,
Icons.favorite_border,
'My Favorites',
onTap: () => _showFavoritesDialog(context, ref),
),
_buildSettingsTile(context, Icons.lock_outline, 'Privacy'),
_buildSettingsTile(
context,
Icons.share_outlined,
'Share with Husband',
onTap: () => _showShareDialog(context, ref),
),
]),
const SizedBox(height: 16),
_buildSettingsGroup(context, 'Cycle', [
_buildSettingsTile(
context, Icons.calendar_today_outlined, 'Cycle Settings'),
_buildSettingsTile(
context, Icons.trending_up_outlined, 'Cycle History'),
_buildSettingsTile(
context, Icons.download_outlined, 'Export Data'),
]),
const SizedBox(height: 16),
_buildSettingsGroup(context, 'Account', [
_buildSettingsTile(context, Icons.logout, 'Reset App / Logout',
onTap: () => _resetApp(context, ref)),
]),
const SizedBox(height: 16),
],
@@ -561,11 +476,8 @@ class _SettingsTab extends ConsumerWidget {
);
}
<<<<<<< HEAD
Widget _buildSettingsGroup(String title, List<Widget> tiles) {
=======
Widget _buildSettingsGroup(BuildContext context, String title, List<Widget> tiles) {
>>>>>>> 6742220 (Your commit message here)
Widget _buildSettingsGroup(
BuildContext context, String title, List<Widget> tiles) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -581,14 +493,10 @@ class _SettingsTab extends ConsumerWidget {
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
<<<<<<< HEAD
color: Colors.white,
borderRadius: BorderRadius.circular(12),
=======
color: Theme.of(context).cardTheme.color,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Theme.of(context).colorScheme.outline.withOpacity(0.05)),
>>>>>>> 6742220 (Your commit message here)
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.05)),
),
child: Column(
children: tiles,
@@ -597,8 +505,121 @@ class _SettingsTab extends ConsumerWidget {
],
);
}
}
<<<<<<< HEAD
=======
>>>>>>> 6742220 (Your commit message here)
void _showFavoritesDialog(BuildContext context, WidgetRef ref) {
final userProfile = ref.read(userProfileProvider);
if (userProfile == null) return;
final controller = TextEditingController(
text: userProfile.favoriteFoods?.join(', ') ?? '',
);
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: [
const Icon(Icons.share_outlined, color: AppColors.sageGreen),
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: AppColors.sageGreen.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.sageGreen.withOpacity(0.3)),
),
child: SelectableText(
pairingCode,
style: GoogleFonts.outfit(
fontSize: 32,
fontWeight: FontWeight.bold,
letterSpacing: 4,
color: AppColors.sageGreen,
),
),
),
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: AppColors.sageGreen,
foregroundColor: Colors.white,
),
child: const Text('Done'),
),
],
),
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:christian_period_tracker/models/user_profile.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../theme/app_theme.dart';
@@ -5,6 +6,10 @@ import '../../models/cycle_entry.dart';
import '../../models/scripture.dart';
import '../../providers/user_provider.dart';
import '../../services/cycle_service.dart';
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 'package:flutter_riverpod/flutter_riverpod.dart';
/// Husband's companion app main screen
@@ -28,10 +33,8 @@ class _HusbandHomeScreenState extends ConsumerState<HusbandHomeScreen> {
index: _selectedIndex,
children: [
const _HusbandDashboard(),
<<<<<<< HEAD
=======
const _HusbandWifeStatus(),
>>>>>>> 6742220 (Your commit message here)
const CalendarScreen(readOnly: true), // Reused Calendar
const HusbandNotesScreen(), // Notes Screen
const _HusbandTipsScreen(),
const _HusbandLearnScreen(),
const _HusbandSettingsScreen(),
@@ -62,14 +65,16 @@ class _HusbandHomeScreenState extends ConsumerState<HusbandHomeScreen> {
label: 'Home',
),
BottomNavigationBarItem(
<<<<<<< HEAD
=======
icon: Icon(Icons.favorite_border),
activeIcon: Icon(Icons.favorite),
label: 'Status',
icon: Icon(Icons.calendar_month_outlined),
activeIcon: Icon(Icons.calendar_month),
label: 'Calendar',
),
BottomNavigationBarItem(
icon: Icon(Icons.note_alt_outlined),
activeIcon: Icon(Icons.note_alt),
label: 'Notes',
),
BottomNavigationBarItem(
>>>>>>> 6742220 (Your commit message here)
icon: Icon(Icons.lightbulb_outline),
activeIcon: Icon(Icons.lightbulb),
label: 'Tips',
@@ -121,20 +126,39 @@ class _HusbandHomeScreenState extends ConsumerState<HusbandHomeScreen> {
}
}
class _HusbandDashboard extends ConsumerWidget {
class _HusbandDashboard extends ConsumerStatefulWidget {
const _HusbandDashboard();
@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<_HusbandDashboard> createState() => _HusbandDashboardState();
}
class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> {
Scripture? _currentScripture;
@override
void initState() {
super.initState();
_loadNewVerse();
}
void _loadNewVerse() {
setState(() {
_currentScripture = ScriptureDatabase().getHusbandScripture();
});
}
@override
Widget build(BuildContext context) {
final user = ref.watch(userProfileProvider);
final cycleInfo = ref.watch(currentCycleInfoProvider);
final wifeName = user?.partnerName ?? "Wife";
final phase = cycleInfo['phase'] as CyclePhase;
final dayOfCycle = cycleInfo['dayOfCycle'] as int;
final daysUntilPeriod = cycleInfo['daysUntilPeriod'] as int;
final scripture = ScriptureDatabase.getHusbandScripture();
final wifeName = user?.partnerName ?? "Wife";
final phase = cycleInfo.phase;
final dayOfCycle = cycleInfo.dayOfCycle;
final daysUntilPeriod = cycleInfo.daysUntilPeriod;
final scripture = _currentScripture ?? ScriptureDatabase().getHusbandScripture();
return SafeArea(
child: SingleChildScrollView(
@@ -311,6 +335,87 @@ class _HusbandDashboard extends ConsumerWidget {
),
const SizedBox(height: 20),
// Recent Cravings (Dynamic)
Builder(
builder: (context) {
// Get recent cravings from the last 3 days
final allEntries = ref.read(cycleEntriesProvider);
// Sort by date desc
final sortedEntries = List<CycleEntry>.from(allEntries)..sort((a,b) => b.date.compareTo(a.date));
final recentCravings = <String>{};
final now = DateTime.now();
for (var entry in sortedEntries) {
if (now.difference(entry.date).inDays > 3) break;
if (entry.cravings != null) {
recentCravings.addAll(entry.cravings!);
}
}
if (recentCravings.isEmpty) return const SizedBox.shrink();
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 20),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.rose.withOpacity(0.3)),
boxShadow: [
BoxShadow(
color: AppColors.rose.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppColors.rose.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.fastfood,
color: AppColors.rose,
size: 20,
),
),
const SizedBox(width: 12),
Text(
'She is Craving...',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: recentCravings.map((craving) => Chip(
label: Text(craving),
backgroundColor: AppColors.rose.withOpacity(0.1),
labelStyle: GoogleFonts.outfit(color: AppColors.navyBlue, fontWeight: FontWeight.w500),
side: BorderSide.none,
)).toList(),
),
],
),
);
},
),
// Scripture for Husbands
Container(
width: double.infinity,
@@ -340,19 +445,47 @@ class _HusbandDashboard extends ConsumerWidget {
size: 20,
),
const SizedBox(width: 8),
Text(
'Scripture for Husbands',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
Expanded(
child: Text(
'Scripture for Husbands',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
),
// Quick version toggle
GestureDetector(
onTap: () => _showVersionPicker(context, ref),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.gold.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
user?.bibleTranslation.label ?? 'ESV',
style: GoogleFonts.outfit(
fontSize: 11,
fontWeight: FontWeight.w600,
color: AppColors.gold,
),
),
const SizedBox(width: 2),
Icon(Icons.arrow_drop_down, color: AppColors.gold, size: 16),
],
),
),
),
],
),
const SizedBox(height: 12),
Text(
'"${scripture.verse}"',
'"${scripture.getVerse(user?.bibleTranslation ?? BibleTranslation.esv)}"',
style: GoogleFonts.lora(
fontSize: 15,
fontStyle: FontStyle.italic,
@@ -361,13 +494,43 @@ class _HusbandDashboard extends ConsumerWidget {
),
),
const SizedBox(height: 8),
Text(
'${scripture.reference}',
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${scripture.reference}',
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
GestureDetector(
onTap: _loadNewVerse,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.gold.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.refresh, color: AppColors.gold, size: 14),
const SizedBox(width: 4),
Text(
'New Verse',
style: GoogleFonts.outfit(
fontSize: 11,
fontWeight: FontWeight.w500,
color: AppColors.gold,
),
),
],
),
),
),
],
),
],
),
@@ -428,6 +591,51 @@ class _HusbandDashboard extends ConsumerWidget {
}
}
void _showVersionPicker(BuildContext context, WidgetRef ref) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Choose Translation',
style: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
const SizedBox(height: 16),
...BibleTranslation.values.map((translation) => ListTile(
title: Text(
translation.label,
style: GoogleFonts.outfit(fontWeight: FontWeight.w500),
),
trailing: ref.watch(userProfileProvider)?.bibleTranslation == translation
? const Icon(Icons.check, color: AppColors.sageGreen)
: null,
onTap: () async {
final profile = ref.read(userProfileProvider);
if (profile != null) {
await ref.read(userProfileProvider.notifier).updateProfile(
profile.copyWith(bibleTranslation: translation),
);
}
if (context.mounted) Navigator.pop(context);
},
)),
],
),
),
);
}
void _showPrayerPrompt(BuildContext context, CyclePhase phase) {
showModalBottomSheet(
context: context,
@@ -527,7 +735,6 @@ class _HusbandTipsScreen extends StatelessWidget {
),
),
const SizedBox(height: 24),
_buildTipCategory('During Her Period', [
'🏠 Help with household tasks without being asked',
'🍵 Bring her favorite comfort drink',
@@ -535,7 +742,6 @@ class _HusbandTipsScreen extends StatelessWidget {
'🙏 Pray for her physical comfort',
]),
const SizedBox(height: 16),
_buildTipCategory('Follicular Phase', [
'🎉 Plan dates or activities—her energy is returning',
'💬 She may be more talkative and social',
@@ -543,7 +749,6 @@ class _HusbandTipsScreen extends StatelessWidget {
'❤️ Affirm her strengths and beauty',
]),
const SizedBox(height: 16),
_buildTipCategory('Luteal Phase (PMS)', [
'😌 Be patient—PMS may affect her mood',
'🍫 Surprise with comfort foods',
@@ -551,6 +756,24 @@ class _HusbandTipsScreen extends StatelessWidget {
'👂 Listen more, "fix" less',
]),
const SizedBox(height: 16),
const SizedBox(height: 16),
// Her Favorites Section
Consumer(
builder: (context, ref, child) {
final user = ref.watch(userProfileProvider);
final favorites = user?.favoriteFoods;
if (favorites == null || favorites.isEmpty) return const SizedBox.shrink();
return Column(
children: [
_buildTipCategory('❤️ Her Favorites (Cheat Sheet)', favorites),
const SizedBox(height: 16),
],
);
},
),
_buildTipCategory('General Wisdom', [
'🗣️ Ask how she\'s feeling—and actually listen',
@@ -621,55 +844,60 @@ class _HusbandLearnScreen extends StatelessWidget {
),
),
const SizedBox(height: 24),
_buildSection('Understanding Her', [
_buildSection(context, 'Understanding Her', [
_LearnItem(
icon: Icons.loop,
title: 'The 4 Phases of Her Cycle',
subtitle: 'What\'s happening in her body each month',
articleId: 'four_phases',
),
_LearnItem(
icon: Icons.psychology_outlined,
title: 'Why Does Her Mood Change?',
subtitle: 'Hormones explained simply',
articleId: 'mood_changes',
),
_LearnItem(
icon: Icons.medical_information_outlined,
title: 'PMS is Real',
subtitle: 'Medical facts for supportive husbands',
articleId: 'pms_is_real',
),
]),
const SizedBox(height: 24),
_buildSection('Biblical Manhood', [
_buildSection(context, 'Biblical Manhood', [
_LearnItem(
icon: Icons.favorite,
title: 'Loving Like Christ',
subtitle: 'Ephesians 5 in daily practice',
articleId: 'loving_like_christ',
),
_LearnItem(
icon: Icons.handshake,
title: 'Servant Leadership at Home',
subtitle: 'What it really means',
articleId: 'servant_leadership',
),
_LearnItem(
icon: Icons.auto_awesome,
title: 'Praying for Your Wife',
subtitle: 'Practical guide',
articleId: 'praying_for_wife',
),
]),
const SizedBox(height: 24),
_buildSection('NFP for Husbands', [
_buildSection(context, 'NFP for Husbands', [
_LearnItem(
icon: Icons.show_chart,
title: 'Reading the Charts Together',
subtitle: 'Understanding fertility signs',
articleId: 'reading_charts',
),
_LearnItem(
icon: Icons.schedule,
title: 'Abstinence as Spiritual Discipline',
subtitle: 'Growing together during fertile days',
articleId: 'abstinence_discipline',
),
]),
],
@@ -678,7 +906,7 @@ class _HusbandLearnScreen extends StatelessWidget {
);
}
Widget _buildSection(String title, List<_LearnItem> items) {
Widget _buildSection(BuildContext context, String title, List<_LearnItem> items) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -732,7 +960,14 @@ class _HusbandLearnScreen extends StatelessWidget {
Icons.chevron_right,
color: AppColors.lightGray,
),
onTap: () {},
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LearnArticleScreen(articleId: item.articleId),
),
);
},
))
.toList(),
),
@@ -746,11 +981,13 @@ class _LearnItem {
final IconData icon;
final String title;
final String subtitle;
final String articleId;
const _LearnItem({
required this.icon,
required this.title,
required this.subtitle,
required this.articleId,
});
}
@@ -762,11 +999,14 @@ class _HusbandSettingsScreen extends ConsumerWidget {
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?'),
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),
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel')),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Reset', style: TextStyle(color: Colors.red)),
),
],
@@ -776,13 +1016,191 @@ class _HusbandSettingsScreen extends ConsumerWidget {
if (confirmed == true) {
await ref.read(userProfileProvider.notifier).clearProfile();
await ref.read(cycleEntriesProvider.notifier).clearEntries();
if (context.mounted) {
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
}
}
}
Future<void> _loadDemoData(BuildContext context, WidgetRef ref) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Load Demo Data?'),
content: const Text(
'This will populate the app with mock cycle entries and a wife profile.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel')),
TextButton(
onPressed: () => Navigator.pop(context, true),
child:
const Text('Load Data', style: TextStyle(color: Colors.blue)),
),
],
),
);
if (confirmed == true) {
final mockService = MockDataService();
// Load mock entries
final entries = mockService.generateMockCycleEntries();
for (var entry in entries) {
await ref.read(cycleEntriesProvider.notifier).addEntry(entry);
}
// Update mock profile
final mockWife = mockService.generateMockWifeProfile();
// Need to preserve current Husband ID and Role but take other data
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);
}
}
}
void _showTranslationPicker(BuildContext context, WidgetRef ref) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Choose Translation',
style: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
const SizedBox(height: 16),
...BibleTranslation.values.map((translation) => ListTile(
title: Text(
translation.label,
style: GoogleFonts.outfit(fontWeight: FontWeight.w500),
),
trailing: ref.watch(userProfileProvider)?.bibleTranslation == translation
? const Icon(Icons.check, color: AppColors.sageGreen)
: null,
onTap: () async {
final profile = ref.read(userProfileProvider);
if (profile != null) {
await ref.read(userProfileProvider.notifier).updateProfile(
profile.copyWith(bibleTranslation: translation),
);
}
if (context.mounted) Navigator.pop(context);
},
)),
],
),
),
);
}
void _showConnectDialog(BuildContext context, WidgetRef ref) {
final codeController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
const Icon(Icons.link, color: AppColors.navyBlue),
const SizedBox(width: 8),
const Text('Connect with Wife'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Enter the pairing code from your wife\'s app:',
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
),
const SizedBox(height: 16),
TextField(
controller: codeController,
decoration: const InputDecoration(
hintText: 'e.g., ABC123',
border: OutlineInputBorder(),
),
textCapitalization: TextCapitalization.characters,
),
const SizedBox(height: 16),
Text(
'Your wife can find this code in her Settings under "Share with Husband".',
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
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);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Connected! Loading wife\'s data...'),
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);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.navyBlue,
foregroundColor: Colors.white,
),
child: const Text('Connect'),
),
],
),
);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return SafeArea(
@@ -808,30 +1226,69 @@ class _HusbandSettingsScreen extends ConsumerWidget {
child: Column(
children: [
ListTile(
leading: const Icon(Icons.notifications_outlined),
title: Text('Notifications', style: GoogleFonts.outfit()),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
leading: const Icon(Icons.notifications_outlined,
color: AppColors.navyBlue),
title: Text('Notifications',
style: GoogleFonts.outfit(fontWeight: FontWeight.w500)),
trailing: Switch(value: true, onChanged: (val) {}),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.link_outlined),
title: Text('Connection', style: GoogleFonts.outfit()),
subtitle: Text('Linked with wife\'s app', style: GoogleFonts.outfit(fontSize: 12)),
leading: const Icon(Icons.link,
color: AppColors.navyBlue),
title: Text('Connect with Wife',
style: GoogleFonts.outfit(fontWeight: FontWeight.w500)),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
onTap: () => _showConnectDialog(context, ref),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.logout),
title: Text('Reset App / Logout', style: GoogleFonts.outfit()),
trailing: const Icon(Icons.chevron_right),
leading: const Icon(Icons.menu_book_outlined,
color: AppColors.navyBlue),
title: Text('Bible Translation',
style: GoogleFonts.outfit(fontWeight: FontWeight.w500)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
ref.watch(userProfileProvider.select((u) => u?.bibleTranslation.label)) ?? 'ESV',
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
const Icon(Icons.chevron_right),
],
),
onTap: () => _showTranslationPicker(context, ref),
),
],
),
),
const SizedBox(height: 20),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
ListTile(
leading: const Icon(Icons.cloud_download_outlined,
color: Colors.blue),
title: Text('Load Demo Data',
style: GoogleFonts.outfit(
fontWeight: FontWeight.w500, color: Colors.blue)),
onTap: () => _loadDemoData(context, ref),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.logout, color: Colors.red),
title: Text('Reset App',
style: GoogleFonts.outfit(
fontWeight: FontWeight.w500, color: Colors.red)),
onTap: () => _resetApp(context, ref),
),
ListTile(
leading: const Icon(Icons.help_outline),
title: Text('Help & Support', style: GoogleFonts.outfit()),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
],
),
),
@@ -841,8 +1298,7 @@ class _HusbandSettingsScreen extends ConsumerWidget {
);
}
}
<<<<<<< HEAD
=======
class _HusbandWifeStatus extends ConsumerWidget {
const _HusbandWifeStatus();
@@ -851,11 +1307,11 @@ class _HusbandWifeStatus extends ConsumerWidget {
final user = ref.watch(userProfileProvider);
final cycleInfo = ref.watch(currentCycleInfoProvider);
final entries = ref.watch(cycleEntriesProvider);
final wifeName = user?.partnerName ?? "Wife";
final phase = cycleInfo['phase'] as CyclePhase;
final dayOfCycle = cycleInfo['dayOfCycle'] as int;
final phase = cycleInfo.phase;
final dayOfCycle = cycleInfo.dayOfCycle;
// Find today's entry
final todayEntry = entries.firstWhere(
(e) => DateUtils.isSameDay(e.date, DateTime.now()),
@@ -960,16 +1416,20 @@ class _HusbandWifeStatus extends ConsumerWidget {
decoration: BoxDecoration(
color: AppColors.navyBlue.withOpacity(0.03),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.navyBlue.withOpacity(0.05)),
border:
Border.all(color: AppColors.navyBlue.withOpacity(0.05)),
),
child: Column(
children: [
if (todayEntry.mood != null)
_buildLogTile(Icons.emoji_emotions_outlined, 'Mood', '${todayEntry.mood!.emoji} ${todayEntry.mood!.label}'),
_buildLogTile(Icons.emoji_emotions_outlined, 'Mood',
'${todayEntry.mood!.emoji} ${todayEntry.mood!.label}'),
if (todayEntry.hasSymptoms)
_buildLogTile(Icons.healing_outlined, 'Symptoms', _getSymptomsSummary(todayEntry)),
_buildLogTile(Icons.healing_outlined, 'Symptoms',
_getSymptomsSummary(todayEntry)),
if (todayEntry.energyLevel != null)
_buildLogTile(Icons.flash_on, 'Energy', '${todayEntry.energyLevel}/5'),
_buildLogTile(Icons.flash_on, 'Energy',
'${todayEntry.energyLevel}/5'),
],
),
),
@@ -986,8 +1446,9 @@ class _HusbandWifeStatus extends ConsumerWidget {
),
),
const SizedBox(height: 12),
..._generateChecklist(todayEntry, phase).map((item) => _buildCheckItem(item)),
..._generateChecklist(todayEntry, phase)
.map((item) => _buildCheckItem(item)),
const SizedBox(height: 40),
],
),
@@ -1050,7 +1511,8 @@ class _HusbandWifeStatus extends ConsumerWidget {
),
child: Row(
children: [
Icon(Icons.check_circle_outline, color: AppColors.sageGreen, size: 20),
Icon(Icons.check_circle_outline,
color: AppColors.sageGreen, size: 20),
const SizedBox(width: 12),
Expanded(
child: Text(
@@ -1065,7 +1527,8 @@ class _HusbandWifeStatus extends ConsumerWidget {
String _getSymptomsSummary(CycleEntry entry) {
List<String> s = [];
if (entry.crampIntensity != null && entry.crampIntensity! > 0) s.add('Cramps');
if (entry.crampIntensity != null && entry.crampIntensity! > 0)
s.add('Cramps');
if (entry.hasHeadache) s.add('Headache');
if (entry.hasBloating) s.add('Bloating');
if (entry.hasFatigue) s.add('Fatigue');
@@ -1075,7 +1538,7 @@ class _HusbandWifeStatus extends ConsumerWidget {
List<String> _generateChecklist(CycleEntry entry, CyclePhase phase) {
List<String> list = [];
// Symptom-based tips
if (entry.crampIntensity != null && entry.crampIntensity! >= 3) {
list.add('Bring her a heating pad or hot water bottle.');
@@ -1083,7 +1546,8 @@ class _HusbandWifeStatus extends ConsumerWidget {
if (entry.hasHeadache) {
list.add('Suggest some quiet time with dimmed lights.');
}
if (entry.hasFatigue || (entry.energyLevel != null && entry.energyLevel! <= 2)) {
if (entry.hasFatigue ||
(entry.energyLevel != null && entry.energyLevel! <= 2)) {
list.add('Take over dinner or household chores tonight.');
}
if (entry.mood == MoodLevel.sad || entry.mood == MoodLevel.verySad) {
@@ -1111,8 +1575,8 @@ class _HusbandWifeStatus extends ConsumerWidget {
break;
}
}
return list.take(4).toList();
}
}
>>>>>>> 6742220 (Your commit message here)

View File

@@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../../models/cycle_entry.dart';
import '../../providers/user_provider.dart';
class HusbandNotesScreen extends ConsumerWidget {
const HusbandNotesScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final entries = ref.watch(cycleEntriesProvider);
final notesEntries = entries
.where((entry) =>
(entry.notes != null && entry.notes!.isNotEmpty) ||
(entry.husbandNotes != null && entry.husbandNotes!.isNotEmpty))
.toList();
// Sort entries by date, newest first
notesEntries.sort((a, b) => b.date.compareTo(a.date));
return Scaffold(
appBar: AppBar(
title: const Text('Notes'),
),
body: notesEntries.isEmpty
? const Center(
child: Text('No notes have been logged yet.'),
)
: ListView.builder(
itemCount: notesEntries.length,
itemBuilder: (context, index) {
final entry = notesEntries[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
DateFormat.yMMMMd().format(entry.date),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
if (entry.notes != null && entry.notes!.isNotEmpty)
_NoteSection(
title: 'Her Notes',
content: entry.notes!,
),
if (entry.husbandNotes != null && entry.husbandNotes!.isNotEmpty)
_NoteSection(
title: 'Your Notes',
content: entry.husbandNotes!,
),
],
),
),
);
},
),
);
}
}
class _NoteSection extends StatelessWidget {
final String title;
final String content;
const _NoteSection({required this.title, required this.content});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
content,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 12),
],
);
}
}

View File

@@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../data/learn_content.dart';
import '../../theme/app_theme.dart';
/// Screen to display full learn article content
class LearnArticleScreen extends StatelessWidget {
final String articleId;
const LearnArticleScreen({super.key, required this.articleId});
@override
Widget build(BuildContext context) {
final article = LearnContent.getArticle(articleId);
if (article == null) {
return Scaffold(
appBar: AppBar(title: const Text('Article Not Found')),
body: const Center(child: Text('Article not found')),
);
}
return Scaffold(
backgroundColor: AppColors.warmCream,
appBar: AppBar(
backgroundColor: AppColors.warmCream,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: AppColors.navyBlue),
onPressed: () => Navigator.pop(context),
),
title: Text(
article.category,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
centerTitle: true,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title
Text(
article.title,
style: GoogleFonts.outfit(
fontSize: 26,
fontWeight: FontWeight.w700,
color: AppColors.navyBlue,
height: 1.2,
),
),
const SizedBox(height: 8),
Text(
article.subtitle,
style: GoogleFonts.outfit(
fontSize: 15,
color: AppColors.warmGray,
),
),
const SizedBox(height: 24),
// Divider
Container(
height: 3,
width: 40,
decoration: BoxDecoration(
color: AppColors.gold,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 24),
// Sections
...article.sections.map((section) => _buildSection(section)),
],
),
),
);
}
Widget _buildSection(LearnSection section) {
return Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (section.heading != null) ...[
Text(
section.heading!,
style: GoogleFonts.outfit(
fontSize: 17,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
const SizedBox(height: 10),
],
_buildRichText(section.content),
],
),
);
}
Widget _buildRichText(String content) {
// Handle basic markdown-like formatting
final List<InlineSpan> spans = [];
final RegExp boldPattern = RegExp(r'\*\*(.*?)\*\*');
int currentIndex = 0;
for (final match in boldPattern.allMatches(content)) {
// Add text before the match
if (match.start > currentIndex) {
spans.add(TextSpan(
text: content.substring(currentIndex, match.start),
style: GoogleFonts.outfit(
fontSize: 15,
color: AppColors.charcoal,
height: 1.7,
),
));
}
// Add bold text
spans.add(TextSpan(
text: match.group(1),
style: GoogleFonts.outfit(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
height: 1.7,
),
));
currentIndex = match.end;
}
// Add remaining text
if (currentIndex < content.length) {
spans.add(TextSpan(
text: content.substring(currentIndex),
style: GoogleFonts.outfit(
fontSize: 15,
color: AppColors.charcoal,
height: 1.7,
),
));
}
return RichText(
text: TextSpan(children: spans),
);
}
}

View File

@@ -2,55 +2,52 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../models/cycle_entry.dart';
import '../../providers/navigation_provider.dart';
import '../../providers/user_provider.dart';
import '../../theme/app_theme.dart';
import 'package:uuid/uuid.dart';
class LogScreen extends ConsumerStatefulWidget {
<<<<<<< HEAD
const LogScreen({super.key});
=======
final DateTime? initialDate;
const LogScreen({super.key, this.initialDate});
>>>>>>> 6742220 (Your commit message here)
@override
ConsumerState<LogScreen> createState() => _LogScreenState();
}
class _LogScreenState extends ConsumerState<LogScreen> {
<<<<<<< HEAD
=======
late DateTime _selectedDate;
String? _existingEntryId;
>>>>>>> 6742220 (Your commit message here)
bool _isPeriodDay = false;
FlowIntensity? _flowIntensity;
MoodLevel? _mood;
int _energyLevel = 3;
int? _energyLevel;
int _crampIntensity = 0;
bool _hasHeadache = false;
bool _hasBloating = false;
bool _hasBreastTenderness = false;
bool _hasFatigue = false;
bool _hasAcne = false;
<<<<<<< HEAD
final TextEditingController _notesController = TextEditingController();
@override
=======
bool _hasLowerBackPain = false;
bool _hasConstipation = false;
bool _hasDiarrhea = false;
bool _hasInsomnia = false;
int _stressLevel = 1;
int? _stressLevel;
final TextEditingController _notesController = TextEditingController();
final TextEditingController _cravingsController = TextEditingController();
// Intimacy tracking
bool _hadIntimacy = false;
bool? _intimacyProtected; // null = no selection, true = protected, false = unprotected
// Hidden field to preserve husband's notes
String? _husbandNotes;
@override
void initState() {
super.initState();
_selectedDate = widget.initialDate ?? DateTime.now();
// Defer data loading to avoid build-time ref.read
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadExistingData();
@@ -79,8 +76,12 @@ class _LogScreenState extends ConsumerState<LogScreen> {
_hasConstipation = entry.hasConstipation;
_hasDiarrhea = entry.hasDiarrhea;
_hasInsomnia = entry.hasInsomnia;
_stressLevel = entry.stressLevel ?? 1;
_stressLevel = entry.stressLevel;
_notesController.text = entry.notes ?? '';
_cravingsController.text = entry.cravings?.join(', ') ?? '';
_husbandNotes = entry.husbandNotes;
_hadIntimacy = entry.hadIntimacy;
_intimacyProtected = entry.intimacyProtected;
});
} catch (_) {
// No existing entry for this day
@@ -88,21 +89,25 @@ class _LogScreenState extends ConsumerState<LogScreen> {
}
@override
>>>>>>> 6742220 (Your commit message here)
void dispose() {
_notesController.dispose();
_cravingsController.dispose();
super.dispose();
}
Future<void> _saveEntry() async {
List<String>? cravings;
if (_cravingsController.text.isNotEmpty) {
cravings = _cravingsController.text
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
}
final entry = CycleEntry(
<<<<<<< HEAD
id: const Uuid().v4(),
date: DateTime.now(),
=======
id: _existingEntryId ?? const Uuid().v4(),
date: _selectedDate,
>>>>>>> 6742220 (Your commit message here)
isPeriodDay: _isPeriodDay,
flowIntensity: _isPeriodDay ? _flowIntensity : null,
mood: _mood,
@@ -113,60 +118,47 @@ class _LogScreenState extends ConsumerState<LogScreen> {
hasBreastTenderness: _hasBreastTenderness,
hasFatigue: _hasFatigue,
hasAcne: _hasAcne,
<<<<<<< HEAD
=======
hasLowerBackPain: _hasLowerBackPain,
hasConstipation: _hasConstipation,
hasDiarrhea: _hasDiarrhea,
hasInsomnia: _hasInsomnia,
stressLevel: _stressLevel > 1 ? _stressLevel : null,
>>>>>>> 6742220 (Your commit message here)
stressLevel: _stressLevel,
notes: _notesController.text.isNotEmpty ? _notesController.text : null,
cravings: cravings,
husbandNotes: _husbandNotes,
hadIntimacy: _hadIntimacy,
intimacyProtected: _hadIntimacy ? _intimacyProtected : null,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
<<<<<<< HEAD
await ref.read(cycleEntriesProvider.notifier).addEntry(entry);
=======
if (_existingEntryId != null) {
await ref.read(cycleEntriesProvider.notifier).updateEntry(entry);
} else {
await ref.read(cycleEntriesProvider.notifier).addEntry(entry);
}
>>>>>>> 6742220 (Your commit message here)
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Entry saved!', style: GoogleFonts.outfit()),
<<<<<<< HEAD
backgroundColor: AppColors.sageGreen,
=======
backgroundColor: AppColors.success,
>>>>>>> 6742220 (Your commit message here)
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
<<<<<<< HEAD
_resetForm();
=======
if (widget.initialDate != null) {
Navigator.pop(context);
} else {
_resetForm();
}
>>>>>>> 6742220 (Your commit message here)
}
}
void _resetForm() {
setState(() {
<<<<<<< HEAD
=======
_existingEntryId = null;
>>>>>>> 6742220 (Your commit message here)
_isPeriodDay = false;
_flowIntensity = null;
_mood = null;
@@ -177,26 +169,24 @@ class _LogScreenState extends ConsumerState<LogScreen> {
_hasBreastTenderness = false;
_hasFatigue = false;
_hasAcne = false;
<<<<<<< HEAD
=======
_hasLowerBackPain = false;
_hasConstipation = false;
_hasDiarrhea = false;
_hasInsomnia = false;
_stressLevel = 1;
>>>>>>> 6742220 (Your commit message here)
_notesController.clear();
_cravingsController.clear();
_husbandNotes = null;
_hadIntimacy = false;
_intimacyProtected = null;
});
}
@override
Widget build(BuildContext context) {
<<<<<<< HEAD
=======
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
>>>>>>> 6742220 (Your commit message here)
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
@@ -204,22 +194,6 @@ class _LogScreenState extends ConsumerState<LogScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
<<<<<<< HEAD
Text(
'How are you feeling?',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
Text(
_formatDate(DateTime.now()),
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
=======
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -245,23 +219,21 @@ class _LogScreenState extends ConsumerState<LogScreen> {
),
if (widget.initialDate == null)
IconButton(
onPressed: () => ref.read(navigationProvider.notifier).setIndex(0),
onPressed: () =>
ref.read(navigationProvider.notifier).setIndex(0),
icon: const Icon(Icons.close),
style: IconButton.styleFrom(
backgroundColor: theme.colorScheme.surfaceVariant.withOpacity(0.5),
backgroundColor:
theme.colorScheme.surfaceVariant.withOpacity(0.5),
),
),
],
>>>>>>> 6742220 (Your commit message here)
),
const SizedBox(height: 24),
// Period Toggle
_buildSectionCard(
<<<<<<< HEAD
=======
context,
>>>>>>> 6742220 (Your commit message here)
title: 'Period',
child: Row(
children: [
@@ -270,11 +242,7 @@ class _LogScreenState extends ConsumerState<LogScreen> {
'Is today a period day?',
style: GoogleFonts.outfit(
fontSize: 16,
<<<<<<< HEAD
color: AppColors.charcoal,
=======
color: theme.colorScheme.onSurface,
>>>>>>> 6742220 (Your commit message here)
),
),
),
@@ -291,10 +259,7 @@ class _LogScreenState extends ConsumerState<LogScreen> {
if (_isPeriodDay) ...[
const SizedBox(height: 16),
_buildSectionCard(
<<<<<<< HEAD
=======
context,
>>>>>>> 6742220 (Your commit message here)
title: 'Flow Intensity',
child: Row(
children: FlowIntensity.values.map((flow) {
@@ -302,31 +267,20 @@ class _LogScreenState extends ConsumerState<LogScreen> {
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _flowIntensity = flow),
<<<<<<< HEAD
child: Container(
=======
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
>>>>>>> 6742220 (Your commit message here)
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isSelected
<<<<<<< HEAD
? AppColors.menstrualPhase.withOpacity(0.2)
: AppColors.lightGray.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
border: isSelected
? Border.all(color: AppColors.menstrualPhase)
: null,
=======
? AppColors.menstrualPhase.withOpacity(isDark ? 0.3 : 0.2)
: theme.colorScheme.surfaceVariant.withOpacity(0.3),
? AppColors.menstrualPhase
.withOpacity(isDark ? 0.3 : 0.2)
: theme.colorScheme.surfaceVariant
.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
border: isSelected
? Border.all(color: AppColors.menstrualPhase)
: Border.all(color: Colors.transparent),
>>>>>>> 6742220 (Your commit message here)
),
child: Column(
children: [
@@ -334,11 +288,7 @@ class _LogScreenState extends ConsumerState<LogScreen> {
Icons.water_drop,
color: isSelected
? AppColors.menstrualPhase
<<<<<<< HEAD
: AppColors.warmGray,
=======
: theme.colorScheme.onSurfaceVariant,
>>>>>>> 6742220 (Your commit message here)
size: 20,
),
const SizedBox(height: 4),
@@ -346,16 +296,12 @@ class _LogScreenState extends ConsumerState<LogScreen> {
flow.label,
style: GoogleFonts.outfit(
fontSize: 11,
<<<<<<< HEAD
color: isSelected
? AppColors.menstrualPhase
: AppColors.warmGray,
=======
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
color: isSelected
? AppColors.menstrualPhase
: theme.colorScheme.onSurfaceVariant,
>>>>>>> 6742220 (Your commit message here)
),
),
],
@@ -372,10 +318,7 @@ class _LogScreenState extends ConsumerState<LogScreen> {
// Mood
_buildSectionCard(
<<<<<<< HEAD
=======
context,
>>>>>>> 6742220 (Your commit message here)
title: 'Mood',
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
@@ -383,29 +326,18 @@ class _LogScreenState extends ConsumerState<LogScreen> {
final isSelected = _mood == mood;
return GestureDetector(
onTap: () => setState(() => _mood = mood),
<<<<<<< HEAD
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected
? AppColors.softGold.withOpacity(0.2)
=======
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected
? AppColors.softGold.withOpacity(isDark ? 0.3 : 0.2)
>>>>>>> 6742220 (Your commit message here)
? AppColors.softGold
.withOpacity(isDark ? 0.3 : 0.2)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: isSelected
? Border.all(color: AppColors.softGold)
<<<<<<< HEAD
: null,
=======
: Border.all(color: Colors.transparent),
>>>>>>> 6742220 (Your commit message here)
),
child: Column(
children: [
@@ -420,16 +352,12 @@ class _LogScreenState extends ConsumerState<LogScreen> {
mood.label,
style: GoogleFonts.outfit(
fontSize: 10,
<<<<<<< HEAD
color: isSelected
? AppColors.softGold
: AppColors.warmGray,
=======
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
color: isSelected
? AppColors.softGold
: theme.colorScheme.onSurfaceVariant,
>>>>>>> 6742220 (Your commit message here)
),
),
],
@@ -442,16 +370,6 @@ class _LogScreenState extends ConsumerState<LogScreen> {
const SizedBox(height: 16),
<<<<<<< HEAD
// Energy Level
_buildSectionCard(
title: 'Energy Level',
child: Column(
children: [
Row(
children: [
const Icon(Icons.battery_1_bar, color: AppColors.warmGray),
=======
// Energy & Stress Levels
_buildSectionCard(
context,
@@ -471,33 +389,18 @@ class _LogScreenState extends ConsumerState<LogScreen> {
),
),
),
>>>>>>> 6742220 (Your commit message here)
Expanded(
child: Slider(
value: _energyLevel.toDouble(),
value: (_energyLevel ?? 3).toDouble(),
min: 1,
max: 5,
divisions: 4,
<<<<<<< HEAD
=======
activeColor: AppColors.sageGreen,
>>>>>>> 6742220 (Your commit message here)
onChanged: (value) {
setState(() => _energyLevel = value.round());
},
),
),
<<<<<<< HEAD
const Icon(Icons.battery_full, color: AppColors.sageGreen),
],
),
Text(
_getEnergyLabel(_energyLevel),
style: GoogleFonts.outfit(
fontSize: 13,
color: AppColors.warmGray,
),
=======
SizedBox(
width: 50,
child: Text(
@@ -527,7 +430,7 @@ class _LogScreenState extends ConsumerState<LogScreen> {
),
Expanded(
child: Slider(
value: _stressLevel.toDouble(),
value: (_stressLevel ?? 1).toDouble(),
min: 1,
max: 5,
divisions: 4,
@@ -540,7 +443,7 @@ class _LogScreenState extends ConsumerState<LogScreen> {
SizedBox(
width: 50,
child: Text(
'$_stressLevel/5',
'${_stressLevel ?? 1}/5',
textAlign: TextAlign.end,
style: GoogleFonts.outfit(
fontSize: 12,
@@ -549,7 +452,6 @@ class _LogScreenState extends ConsumerState<LogScreen> {
),
),
],
>>>>>>> 6742220 (Your commit message here)
),
],
),
@@ -559,10 +461,7 @@ class _LogScreenState extends ConsumerState<LogScreen> {
// Symptoms
_buildSectionCard(
<<<<<<< HEAD
=======
context,
>>>>>>> 6742220 (Your commit message here)
title: 'Symptoms',
child: Column(
children: [
@@ -575,11 +474,7 @@ class _LogScreenState extends ConsumerState<LogScreen> {
'Cramps',
style: GoogleFonts.outfit(
fontSize: 14,
<<<<<<< HEAD
color: AppColors.charcoal,
=======
color: theme.colorScheme.onSurface,
>>>>>>> 6742220 (Your commit message here)
),
),
),
@@ -596,22 +491,15 @@ class _LogScreenState extends ConsumerState<LogScreen> {
),
),
SizedBox(
<<<<<<< HEAD
width: 40,
child: Text(
_crampIntensity == 0 ? 'None' : '$_crampIntensity/5',
style: GoogleFonts.outfit(
fontSize: 12,
color: AppColors.warmGray,
=======
width: 50,
child: Text(
_crampIntensity == 0 ? 'None' : '$_crampIntensity/5',
_crampIntensity == 0
? 'None'
: '$_crampIntensity/5',
textAlign: TextAlign.end,
style: GoogleFonts.outfit(
fontSize: 11,
color: theme.colorScheme.onSurfaceVariant,
>>>>>>> 6742220 (Your commit message here)
),
),
),
@@ -623,23 +511,29 @@ class _LogScreenState extends ConsumerState<LogScreen> {
spacing: 8,
runSpacing: 8,
children: [
<<<<<<< HEAD
_buildSymptomChip('Headache', _hasHeadache, (v) => setState(() => _hasHeadache = v)),
_buildSymptomChip('Bloating', _hasBloating, (v) => setState(() => _hasBloating = v)),
_buildSymptomChip('Breast Tenderness', _hasBreastTenderness, (v) => setState(() => _hasBreastTenderness = v)),
_buildSymptomChip('Fatigue', _hasFatigue, (v) => setState(() => _hasFatigue = v)),
_buildSymptomChip('Acne', _hasAcne, (v) => setState(() => _hasAcne = v)),
=======
_buildSymptomChip(context, 'Headache', _hasHeadache, (v) => setState(() => _hasHeadache = v)),
_buildSymptomChip(context, 'Bloating', _hasBloating, (v) => setState(() => _hasBloating = v)),
_buildSymptomChip(context, 'Breast Tenderness', _hasBreastTenderness, (v) => setState(() => _hasBreastTenderness = v)),
_buildSymptomChip(context, 'Fatigue', _hasFatigue, (v) => setState(() => _hasFatigue = v)),
_buildSymptomChip(context, 'Acne', _hasAcne, (v) => setState(() => _hasAcne = v)),
_buildSymptomChip(context, 'Back Pain', _hasLowerBackPain, (v) => setState(() => _hasLowerBackPain = v)),
_buildSymptomChip(context, 'Constipation', _hasConstipation, (v) => setState(() => _hasConstipation = v)),
_buildSymptomChip(context, 'Diarrhea', _hasDiarrhea, (v) => setState(() => _hasDiarrhea = v)),
_buildSymptomChip(context, 'Insomnia', _hasInsomnia, (v) => setState(() => _hasInsomnia = v)),
>>>>>>> 6742220 (Your commit message here)
_buildSymptomChip(context, 'Headache', _hasHeadache,
(v) => setState(() => _hasHeadache = v)),
_buildSymptomChip(context, 'Bloating', _hasBloating,
(v) => setState(() => _hasBloating = v)),
_buildSymptomChip(context, 'Breast Tenderness',
_hasBreastTenderness,
(v) => setState(() => _hasBreastTenderness = v)),
_buildSymptomChip(context, 'Fatigue', _hasFatigue,
(v) => setState(() => _hasFatigue = v)),
_buildSymptomChip(context, 'Acne', _hasAcne,
(v) => setState(() => _hasAcne = v)),
_buildSymptomChip(context, 'Back Pain',
_hasLowerBackPain,
(v) => setState(() => _hasLowerBackPain = v)),
_buildSymptomChip(
context,
'Constipation',
_hasConstipation,
(v) => setState(() => _hasConstipation = v)),
_buildSymptomChip(context, 'Diarrhea', _hasDiarrhea,
(v) => setState(() => _hasDiarrhea = v)),
_buildSymptomChip(context, 'Insomnia', _hasInsomnia,
(v) => setState(() => _hasInsomnia = v)),
],
),
],
@@ -648,31 +542,138 @@ class _LogScreenState extends ConsumerState<LogScreen> {
const SizedBox(height: 16),
// Notes
// Cravings
_buildSectionCard(
<<<<<<< HEAD
=======
context,
>>>>>>> 6742220 (Your commit message here)
title: 'Notes',
title: 'Cravings',
child: TextField(
controller: _notesController,
maxLines: 3,
controller: _cravingsController,
decoration: InputDecoration(
hintText: 'Add any notes about how you\'re feeling...',
<<<<<<< HEAD
hintStyle: GoogleFonts.outfit(
color: AppColors.lightGray,
fontSize: 14,
),
border: InputBorder.none,
),
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.charcoal,
=======
hintText: 'e.g., Chocolate, salty chips (comma separated)',
filled: true,
fillColor: isDark ? theme.colorScheme.surface : theme.colorScheme.surfaceVariant.withOpacity(0.1),
fillColor: isDark
? theme.colorScheme.surface
: theme.colorScheme.surfaceVariant.withOpacity(0.1),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
style: GoogleFonts.outfit(
fontSize: 14,
color: theme.colorScheme.onSurface,
),
),
),
const SizedBox(height: 16),
// Intimacy Tracking (for married users)
_buildSectionCard(
context,
title: 'Intimacy',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
title: Text('Had Intimacy Today', style: GoogleFonts.outfit(fontSize: 14)),
value: _hadIntimacy,
onChanged: (val) => setState(() {
_hadIntimacy = val;
if (!val) _intimacyProtected = null;
}),
activeColor: AppColors.sageGreen,
contentPadding: EdgeInsets.zero,
),
if (_hadIntimacy) ...[
const SizedBox(height: 8),
Text('Protection:', style: GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray)),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => setState(() => _intimacyProtected = true),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: _intimacyProtected == true
? AppColors.sageGreen.withOpacity(0.2)
: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _intimacyProtected == true
? AppColors.sageGreen
: Colors.grey.withOpacity(0.3),
),
),
child: Center(
child: Text(
'Protected',
style: GoogleFonts.outfit(
fontWeight: FontWeight.w500,
color: _intimacyProtected == true
? AppColors.sageGreen
: AppColors.warmGray,
),
),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: GestureDetector(
onTap: () => setState(() => _intimacyProtected = false),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: _intimacyProtected == false
? AppColors.rose.withOpacity(0.15)
: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _intimacyProtected == false
? AppColors.rose
: Colors.grey.withOpacity(0.3),
),
),
child: Center(
child: Text(
'Unprotected',
style: GoogleFonts.outfit(
fontWeight: FontWeight.w500,
color: _intimacyProtected == false
? AppColors.rose
: AppColors.warmGray,
),
),
),
),
),
),
],
),
],
],
),
),
const SizedBox(height: 16),
// Notes
_buildSectionCard(
context,
title: 'Notes',
child: TextField(
controller: _notesController,
maxLines: 3,
decoration: InputDecoration(
hintText: 'Add any notes about how you\'re feeling...',
filled: true,
fillColor: isDark
? theme.colorScheme.surface
: theme.colorScheme.surfaceVariant.withOpacity(0.1),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
@@ -681,7 +682,6 @@ class _LogScreenState extends ConsumerState<LogScreen> {
style: GoogleFonts.outfit(
fontSize: 14,
color: theme.colorScheme.onSurface,
>>>>>>> 6742220 (Your commit message here)
),
),
),
@@ -691,10 +691,7 @@ class _LogScreenState extends ConsumerState<LogScreen> {
// Save Button
SizedBox(
width: double.infinity,
<<<<<<< HEAD
=======
height: 54,
>>>>>>> 6742220 (Your commit message here)
child: ElevatedButton(
onPressed: _saveEntry,
child: const Text('Save Entry'),
@@ -707,36 +704,27 @@ class _LogScreenState extends ConsumerState<LogScreen> {
);
}
<<<<<<< HEAD
Widget _buildSectionCard({required String title, required Widget child}) {
=======
Widget _buildSectionCard(BuildContext context, {required String title, required Widget child}) {
Widget _buildSectionCard(BuildContext context,
{required String title, required Widget child}) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
>>>>>>> 6742220 (Your commit message here)
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
<<<<<<< HEAD
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.05),
=======
color: theme.cardTheme.color,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.colorScheme.outline.withOpacity(0.05)),
boxShadow: isDark ? null : [
BoxShadow(
color: Colors.black.withOpacity(0.05),
>>>>>>> 6742220 (Your commit message here)
blurRadius: 10,
offset: const Offset(0, 4),
),
],
boxShadow: isDark
? null
: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -746,11 +734,7 @@ class _LogScreenState extends ConsumerState<LogScreen> {
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
<<<<<<< HEAD
color: AppColors.charcoal,
=======
color: theme.colorScheme.onSurface,
>>>>>>> 6742220 (Your commit message here)
),
),
const SizedBox(height: 12),
@@ -760,25 +744,8 @@ class _LogScreenState extends ConsumerState<LogScreen> {
);
}
<<<<<<< HEAD
Widget _buildSymptomChip(String label, bool isSelected, ValueChanged<bool> onChanged) {
return GestureDetector(
onTap: () => onChanged(!isSelected),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? AppColors.lavender.withOpacity(0.3) : AppColors.lightGray.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: isSelected ? Border.all(color: AppColors.lavender) : null,
),
child: Text(
label,
style: GoogleFonts.outfit(
fontSize: 13,
color: isSelected ? AppColors.ovulationPhase : AppColors.warmGray,
fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400,
=======
Widget _buildSymptomChip(BuildContext context, String label, bool isSelected, ValueChanged<bool> onChanged) {
Widget _buildSymptomChip(BuildContext context, String label, bool isSelected,
ValueChanged<bool> onChanged) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
@@ -791,22 +758,23 @@ class _LogScreenState extends ConsumerState<LogScreen> {
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: isSelected
? theme.colorScheme.tertiary.withOpacity(isDark ? 0.3 : 0.2)
color: isSelected
? theme.colorScheme.tertiary.withOpacity(isDark ? 0.3 : 0.2)
: theme.colorScheme.surfaceVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(20),
border: isSelected
? Border.all(color: theme.colorScheme.tertiary)
border: isSelected
? Border.all(color: theme.colorScheme.tertiary)
: Border.all(color: Colors.transparent),
),
child: Text(
label,
style: GoogleFonts.outfit(
fontSize: 13,
color: isSelected ? theme.colorScheme.onSurface : theme.colorScheme.onSurfaceVariant,
color: isSelected
? theme.colorScheme.onSurface
: theme.colorScheme.onSurfaceVariant,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
),
>>>>>>> 6742220 (Your commit message here)
),
),
),
@@ -814,31 +782,42 @@ class _LogScreenState extends ConsumerState<LogScreen> {
}
String _formatDate(DateTime date) {
<<<<<<< HEAD
=======
final now = DateTime.now();
if (DateUtils.isSameDay(date, now)) {
return 'Today, ${_getMonth(date.month)} ${date.day}';
}
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const days = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday'
];
return '${days[date.weekday - 1]}, ${_getMonth(date.month)} ${date.day}';
}
String _getMonth(int month) {
>>>>>>> 6742220 (Your commit message here)
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'
];
<<<<<<< HEAD
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
return '${days[date.weekday - 1]}, ${months[date.month - 1]} ${date.day}';
=======
return months[month - 1];
>>>>>>> 6742220 (Your commit message here)
}
String _getEnergyLabel(int level) {
String _getEnergyLabel(int? level) {
if (level == null) return 'Not logged';
switch (level) {
case 1:
return 'Very Low';

File diff suppressed because it is too large Load Diff

View File

@@ -45,7 +45,7 @@ class _SplashScreenState extends ConsumerState<SplashScreen> with SingleTickerPr
_controller.forward();
// Navigate after splash
Future.delayed(const Duration(milliseconds: 2500), () {
Future.delayed(const Duration(milliseconds: 1200), () {
_navigateToNextScreen();
});
}

View File

@@ -0,0 +1,166 @@
import 'package:flutter/services.dart' show rootBundle;
import 'package:xml/xml.dart';
import '../models/scripture.dart'; // Assuming Scripture model might need BibleTranslation
class BibleXmlParser {
// Map of common Bible book names to their standard abbreviations or keys used in XML
// This will help in matching references like "Matthew 11:28" to XML structure.
static const Map<String, String> _bookAbbreviations = {
'genesis': 'Gen', 'exodus': 'Exod', 'leviticus': 'Lev', 'numbers': 'Num',
'deuteronomy': 'Deut', 'joshua': 'Josh', 'judges': 'Judg', 'ruth': 'Ruth',
'1 samuel': '1Sam', '2 samuel': '2Sam', '1 kings': '1Kgs', '2 kings': '2Kgs',
'1 chronicles': '1Chr', '2 chronicles': '2Chr', 'ezra': 'Ezra', 'nehemiah': 'Neh',
'esther': 'Esth', 'job': 'Job', 'psalm': 'Ps', 'proverbs': 'Prov',
'ecclesiastes': 'Eccl', 'song of solomon': 'Song', 'isaiah': 'Isa', 'jeremiah': 'Jer',
'lamentations': 'Lam', 'ezekiel': 'Ezek', 'daniel': 'Dan', 'hosea': 'Hos',
'joel': 'Joel', 'amos': 'Amos', 'obadiah': 'Obad', 'jonah': 'Jonah',
'micah': 'Mic', 'nahum': 'Nah', 'habakkuk': 'Hab', 'zephaniah': 'Zeph',
'haggai': 'Hag', 'zechariah': 'Zech', 'malachi': 'Mal',
'matthew': 'Matt', 'mark': 'Mark', 'luke': 'Luke', 'john': 'John',
'acts': 'Acts', 'romans': 'Rom', '1 corinthians': '1Cor', '2 corinthians': '2Cor',
'galatians': 'Gal', 'ephesians': 'Eph', 'philippians': 'Phil', 'colossians': 'Col',
'1 thessalonians': '1Thess', '2 thessalonians': '2Thess', '1 timothy': '1Tim',
'2 timothy': '2Tim', 'titus': 'Titus', 'philemon': 'Phlm', 'hebrews': 'Heb',
'james': 'Jas', '1 peter': '1Pet', '2 peter': '2Pet', '1 john': '1John',
'2 john': '2John', '3 john': '3John', 'jude': 'Jude', 'revelation': 'Rev',
// Add more common names/abbreviations if necessary
};
/// Parses a Bible reference string (e.g., "Matthew 11:28") into its components.
static Map<String, String>? parseReference(String reference) {
final parts = reference.split(' ');
if (parts.length < 2) return null; // Needs at least Book and Chapter:Verse
String book = parts.sublist(0, parts.length - 1).join(' ').toLowerCase();
String chapterVerse = parts.last;
final chapterVerseParts = chapterVerse.split(':');
if (chapterVerseParts.length != 2) return null; // Must have Chapter:Verse
return {
'book': book,
'chapter': chapterVerseParts[0],
'verse': chapterVerseParts[1],
};
}
// Cache for parsed XML documents to avoid reloading/reparsing
static final Map<String, XmlDocument> _xmlCache = {};
/// Loads an XML Bible file from assets and returns the parsed document.
Future<XmlDocument> loadXmlAsset(String assetPath) async {
if (_xmlCache.containsKey(assetPath)) {
return _xmlCache[assetPath]!;
}
print('Loading and parsing XML asset: $assetPath'); // Debug log
final String xmlString = await rootBundle.loadString(assetPath);
final document = XmlDocument.parse(xmlString);
_xmlCache[assetPath] = document;
return document;
}
/// Extracts a specific verse from a parsed XML document.
/// Supports two schemas:
/// 1. <XMLBIBLE><BIBLEBOOK bname="..."><CHAPTER cnumber="..."><VERS vnumber="...">
/// 2. <bible><b n="..."><c n="..."><v n="...">
/// Extracts a specific verse from a parsed XML document.
/// Supports two schemas:
/// 1. <XMLBIBLE><BIBLEBOOK bname="..."><CHAPTER cnumber="..."><VERS vnumber="...">
/// 2. <bible><b n="..."><c n="..."><v n="...">
String? getVerseFromXml(XmlDocument document, String bookName, int chapterNum, int verseNum) {
// Standardize book name for lookup
String lookupBookName = _bookAbbreviations[bookName.toLowerCase()] ?? bookName;
// Use root element to avoid full document search
XmlElement root = document.rootElement;
// -- Find Book --
// Try Schema 1: Direct child of root
var bookElement = root.findElements('BIBLEBOOK').firstWhere(
(element) {
final nameAttr = element.getAttribute('bname');
return nameAttr?.toLowerCase() == lookupBookName.toLowerCase() ||
nameAttr?.toLowerCase() == bookName.toLowerCase();
},
orElse: () => XmlElement(XmlName('notfound')),
);
// Try Schema 2 if not found
if (bookElement.name.local == 'notfound') {
bookElement = root.findElements('b').firstWhere(
(element) {
final nameAttr = element.getAttribute('n');
return nameAttr?.toLowerCase() == lookupBookName.toLowerCase() ||
nameAttr?.toLowerCase() == bookName.toLowerCase();
},
orElse: () => XmlElement(XmlName('notfound')),
);
}
if (bookElement.name.local == 'notfound') {
// print('Book "$bookName" not found in XML.'); // Commented out to reduce log spam
return null;
}
// -- Find Chapter --
// Try Schema 1: Direct child of book
var chapterElement = bookElement.findElements('CHAPTER').firstWhere(
(element) => element.getAttribute('cnumber') == chapterNum.toString(),
orElse: () => XmlElement(XmlName('notfound')),
);
// Try Schema 2 if not found
if (chapterElement.name.local == 'notfound') {
chapterElement = bookElement.findElements('c').firstWhere(
(element) => element.getAttribute('n') == chapterNum.toString(),
orElse: () => XmlElement(XmlName('notfound')),
);
}
if (chapterElement.name.local == 'notfound') {
// print('Chapter "$chapterNum" not found for book "$bookName".');
return null;
}
// -- Find Verse --
// Try Schema 1: Direct child of chapter
var verseElement = chapterElement.findElements('VERS').firstWhere(
(element) => element.getAttribute('vnumber') == verseNum.toString(),
orElse: () => XmlElement(XmlName('notfound')),
);
// Try Schema 2 if not found
if (verseElement.name.local == 'notfound') {
verseElement = chapterElement.findElements('v').firstWhere(
(element) => element.getAttribute('n') == verseNum.toString(),
orElse: () => XmlElement(XmlName('notfound')),
);
}
if (verseElement.name.local == 'notfound') {
// print('Verse "$verseNum" not found for Chapter "$chapterNum", book "$bookName".');
return null;
}
// Extract the text content of the verse
return verseElement.innerText.trim();
}
/// Retrieves a specific verse from an XML asset file.
Future<String?> getVerseFromAsset(String assetPath, String reference) async {
final parsedRef = parseReference(reference);
if (parsedRef == null) {
print('Invalid reference format: $reference');
return null;
}
final document = await loadXmlAsset(assetPath);
final bookName = parsedRef['book']!;
final chapterNum = int.parse(parsedRef['chapter']!);
final verseNum = int.parse(parsedRef['verse']!);
return getVerseFromXml(document, bookName, chapterNum, verseNum);
}
}

View File

@@ -1,16 +1,47 @@
import '../models/user_profile.dart';
import '../models/cycle_entry.dart';
class CycleInfo {
final CyclePhase phase;
final int dayOfCycle;
final int daysUntilPeriod;
final bool isPeriodExpected;
CycleInfo({
required this.phase,
required this.dayOfCycle,
required this.daysUntilPeriod,
required this.isPeriodExpected,
});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CycleInfo &&
runtimeType == other.runtimeType &&
phase == other.phase &&
dayOfCycle == other.dayOfCycle &&
daysUntilPeriod == other.daysUntilPeriod &&
isPeriodExpected == other.isPeriodExpected;
@override
int get hashCode =>
phase.hashCode ^
dayOfCycle.hashCode ^
daysUntilPeriod.hashCode ^
isPeriodExpected.hashCode;
}
class CycleService {
/// Calculates the current cycle information based on user profile
static Map<String, dynamic> calculateCycleInfo(UserProfile? user) {
static CycleInfo calculateCycleInfo(UserProfile? user) {
if (user?.lastPeriodStartDate == null) {
return {
'phase': CyclePhase.follicular,
'dayOfCycle': 1,
'daysUntilPeriod': user?.averageCycleLength ?? 28,
'isPeriodExpected': false,
};
return CycleInfo(
phase: CyclePhase.follicular,
dayOfCycle: 1,
daysUntilPeriod: user?.averageCycleLength ?? 28,
isPeriodExpected: false,
);
}
final lastPeriod = user!.lastPeriodStartDate!;
@@ -38,12 +69,12 @@ class CycleService {
phase = CyclePhase.luteal;
}
return {
'phase': phase,
'dayOfCycle': dayOfCycle,
'daysUntilPeriod': daysUntilPeriod,
'isPeriodExpected': daysUntilPeriod <= 0 || dayOfCycle <= 5,
};
return CycleInfo(
phase: phase,
dayOfCycle: dayOfCycle,
daysUntilPeriod: daysUntilPeriod,
isPeriodExpected: daysUntilPeriod <= 0 || dayOfCycle <= 5,
);
}
/// Format cycle day for display

View File

@@ -0,0 +1,99 @@
import 'dart:math';
import 'package:uuid/uuid.dart';
import '../models/cycle_entry.dart';
import '../models/user_profile.dart';
class MockDataService {
final Random _random = Random();
final Uuid _uuid = const Uuid();
UserProfile generateMockWifeProfile() {
return UserProfile(
id: _uuid.v4(),
name: 'Sarah',
relationshipStatus: RelationshipStatus.married,
averageCycleLength: 29,
averagePeriodLength: 5,
lastPeriodStartDate: DateTime.now().subtract(const Duration(days: 10)),
favoriteFoods: ['Chocolate', 'Ice Cream', 'Berries'],
isDataShared: true,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
}
List<CycleEntry> generateMockCycleEntries({
int days = 90,
int cycleLength = 28,
int periodLength = 5,
}) {
final List<CycleEntry> entries = [];
final DateTime today = DateTime.now();
for (int i = 0; i < days; i++) {
final DateTime date = today.subtract(Duration(days: i));
final int dayOfCycle = (cycleLength - (i % cycleLength)) % cycleLength;
bool isPeriodDay = dayOfCycle < periodLength;
FlowIntensity? flow;
if (isPeriodDay) {
if (dayOfCycle < 2) {
flow = FlowIntensity.heavy;
} else if (dayOfCycle < 4) {
flow = FlowIntensity.medium;
} else {
flow = FlowIntensity.light;
}
}
final entry = CycleEntry(
id: _uuid.v4(),
date: date,
isPeriodDay: isPeriodDay,
flowIntensity: flow,
mood: MoodLevel.values[_random.nextInt(MoodLevel.values.length)],
energyLevel: _random.nextInt(5) + 1,
crampIntensity: isPeriodDay && _random.nextBool() ? _random.nextInt(4) + 1 : 0,
hasHeadache: !isPeriodDay && _random.nextDouble() < 0.2,
hasBloating: !isPeriodDay && _random.nextDouble() < 0.3,
hasBreastTenderness: dayOfCycle > 20 && _random.nextDouble() < 0.4,
hasFatigue: _random.nextDouble() < 0.3,
hasAcne: dayOfCycle > 18 && _random.nextDouble() < 0.25,
hasLowerBackPain: isPeriodDay && _random.nextDouble() < 0.4,
stressLevel: _random.nextInt(5) + 1,
notes: _getNoteForDay(dayOfCycle, cycleLength),
husbandNotes: _getHusbandNoteForDay(dayOfCycle),
createdAt: date,
updatedAt: date,
);
entries.add(entry);
}
return entries.reversed.toList();
}
String? _getNoteForDay(int dayOfCycle, int cycleLength) {
if (_random.nextDouble() < 0.3) { // 30% chance of having a note
if (dayOfCycle < 5) {
return "Feeling a bit tired and crampy today. Taking it easy.";
} else if (dayOfCycle > 10 && dayOfCycle < 16) {
return "Feeling energetic and positive! Productive day at work.";
} else if (dayOfCycle > cycleLength - 7) {
return "A bit irritable today, craving some chocolate.";
} else {
return "Just a regular day. Nothing much to report.";
}
}
return null;
}
String? _getHusbandNoteForDay(int dayOfCycle) {
if (_random.nextDouble() < 0.2) { // 20% chance of husband note
if (dayOfCycle < 5) {
return "She seems to be in a bit of pain. I'll make her some tea.";
} else if (dayOfCycle > 22) {
return "She mentioned feeling a little down. Extra hugs tonight.";
}
}
return null;
}
}

View File

@@ -10,25 +10,25 @@ class AppColors {
static const Color lavender = Color(0xFFD4C4E8);
static const Color cream = Color(0xFFFDF8F5);
static const Color softGold = Color(0xFFD4A574);
// Text Colors
static const Color charcoal = Color(0xFF3D3D3D);
static const Color warmGray = Color(0xFF7A7A7A);
static const Color lightGray = Color(0xFFB8B8B8);
// Husband's App Colors
static const Color navyBlue = Color(0xFF2C3E50);
static const Color steelBlue = Color(0xFF5D7B93);
static const Color warmCream = Color(0xFFF5F0E8);
static const Color gold = Color(0xFFC9A961);
static const Color softCoral = Color(0xFFE8B4A8);
// Phase Colors
static const Color menstrualPhase = Color(0xFFE88A9E);
static const Color follicularPhase = Color(0xFF8BC5A3);
static const Color ovulationPhase = Color(0xFFB8A5D4);
static const Color lutealPhase = Color(0xFF8BA5C5);
// Semantic Colors
static const Color success = Color(0xFF7AB98A);
static const Color warning = Color(0xFFE8C567);
@@ -42,7 +42,7 @@ class AppTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
// Color Scheme
colorScheme: const ColorScheme.light(
primary: AppColors.sageGreen,
@@ -54,10 +54,10 @@ class AppTheme {
onSecondary: Colors.white,
onSurface: AppColors.charcoal,
),
// Scaffold
scaffoldBackgroundColor: AppColors.cream,
// AppBar
appBarTheme: AppBarTheme(
backgroundColor: AppColors.cream,
@@ -70,7 +70,7 @@ class AppTheme {
color: AppColors.charcoal,
),
),
// Text Theme
textTheme: TextTheme(
displayLarge: GoogleFonts.outfit(
@@ -124,9 +124,9 @@ class AppTheme {
color: AppColors.charcoal,
),
),
// Card Theme
cardTheme: CardTheme(
cardTheme: CardThemeData(
color: Colors.white,
elevation: 2,
shadowColor: AppColors.charcoal.withOpacity(0.1),
@@ -135,7 +135,7 @@ class AppTheme {
),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
// Button Themes
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
@@ -152,7 +152,7 @@ class AppTheme {
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.sageGreen,
@@ -167,7 +167,7 @@ class AppTheme {
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: AppColors.rose,
@@ -177,7 +177,7 @@ class AppTheme {
),
),
),
// Input Decoration
inputDecorationTheme: InputDecorationTheme(
filled: true,
@@ -194,13 +194,14 @@ class AppTheme {
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.sageGreen, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
hintStyle: GoogleFonts.outfit(
color: AppColors.lightGray,
fontSize: 14,
),
),
// Bottom Navigation
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: Colors.white,
@@ -217,14 +218,14 @@ class AppTheme {
fontWeight: FontWeight.w400,
),
),
// Floating Action Button
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: AppColors.sageGreen,
foregroundColor: Colors.white,
elevation: 4,
),
// Slider Theme
sliderTheme: SliderThemeData(
activeTrackColor: AppColors.sageGreen,
@@ -233,7 +234,7 @@ class AppTheme {
overlayColor: AppColors.sageGreen.withOpacity(0.2),
trackHeight: 4,
),
// Divider
dividerTheme: DividerThemeData(
color: AppColors.lightGray.withOpacity(0.3),
@@ -242,24 +243,12 @@ class AppTheme {
),
);
}
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
<<<<<<< HEAD
colorScheme: const ColorScheme.dark(
primary: AppColors.sageGreen,
secondary: AppColors.rose,
tertiary: AppColors.lavender,
surface: Color(0xFF1E1E1E),
error: AppColors.error,
),
scaffoldBackgroundColor: const Color(0xFF121212),
=======
// Color Scheme
colorScheme: ColorScheme.dark(
primary: AppColors.sageGreen,
@@ -273,10 +262,10 @@ class AppTheme {
onSurfaceVariant: Colors.white70,
outline: Colors.white.withOpacity(0.1),
),
// Scaffold
scaffoldBackgroundColor: const Color(0xFF121212),
// AppBar
appBarTheme: AppBarTheme(
backgroundColor: const Color(0xFF121212),
@@ -289,17 +278,14 @@ class AppTheme {
color: Colors.white,
),
),
// Text Theme
>>>>>>> 6742220 (Your commit message here)
textTheme: TextTheme(
displayLarge: GoogleFonts.outfit(
fontSize: 32,
fontWeight: FontWeight.w600,
color: Colors.white,
),
<<<<<<< HEAD
=======
displayMedium: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
@@ -325,7 +311,6 @@ class AppTheme {
fontWeight: FontWeight.w500,
color: Colors.white,
),
>>>>>>> 6742220 (Your commit message here)
bodyLarge: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w400,
@@ -336,16 +321,6 @@ class AppTheme {
fontWeight: FontWeight.w400,
color: Colors.white70,
),
<<<<<<< HEAD
),
cardTheme: CardTheme(
color: const Color(0xFF1E1E1E),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
=======
bodySmall: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w400,
@@ -357,9 +332,9 @@ class AppTheme {
color: Colors.white,
),
),
// Card Theme
cardTheme: CardTheme(
cardTheme: CardThemeData(
color: const Color(0xFF1E1E1E),
elevation: 0, // Material 3 uses color/opacity for elevation in dark mode
shadowColor: Colors.transparent,
@@ -418,13 +393,14 @@ class AppTheme {
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.sageGreen, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
hintStyle: GoogleFonts.outfit(
color: Colors.white38,
fontSize: 14,
),
),
// Bottom Navigation
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: const Color(0xFF1E1E1E),
@@ -459,7 +435,6 @@ class AppTheme {
color: Colors.white.withOpacity(0.05),
thickness: 1,
space: 24,
>>>>>>> 6742220 (Your commit message here)
),
);
}
@@ -467,35 +442,21 @@ class AppTheme {
/// Scripture text style
TextStyle scriptureStyle(BuildContext context, {double? fontSize}) {
<<<<<<< HEAD
return GoogleFonts.lora(
fontSize: fontSize ?? 16,
fontStyle: FontStyle.italic,
color: AppColors.charcoal,
=======
final isDark = Theme.of(context).brightness == Brightness.dark;
return GoogleFonts.lora(
fontSize: fontSize ?? 16,
fontStyle: FontStyle.italic,
color: isDark ? Colors.white : AppColors.charcoal,
>>>>>>> 6742220 (Your commit message here)
height: 1.6,
);
}
/// Scripture reference style
TextStyle scriptureRefStyle(BuildContext context) {
<<<<<<< HEAD
return GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
=======
final isDark = Theme.of(context).brightness == Brightness.dark;
return GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: isDark ? Colors.white54 : AppColors.warmGray,
>>>>>>> 6742220 (Your commit message here)
);
}

View File

@@ -4,11 +4,7 @@ import 'dart:math' as math;
import '../theme/app_theme.dart';
import '../models/cycle_entry.dart';
<<<<<<< HEAD
class CycleRing extends StatelessWidget {
=======
class CycleRing extends StatefulWidget {
>>>>>>> 6742220 (Your commit message here)
final int dayOfCycle;
final int totalDays;
final CyclePhase phase;
@@ -21,77 +17,11 @@ class CycleRing extends StatefulWidget {
});
@override
<<<<<<< HEAD
Widget build(BuildContext context) {
final progress = dayOfCycle / totalDays;
final daysUntilNextPeriod = totalDays - dayOfCycle;
return Container(
width: 220,
height: 220,
child: Stack(
alignment: Alignment.center,
children: [
// Background ring
CustomPaint(
size: const Size(220, 220),
painter: _CycleRingPainter(
progress: progress,
phase: phase,
),
),
// Center content
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Day $dayOfCycle',
style: GoogleFonts.outfit(
fontSize: 32,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
phase.emoji,
style: const TextStyle(fontSize: 14),
),
const SizedBox(width: 6),
Text(
phase.label,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: _getPhaseColor(phase),
),
),
],
),
),
const SizedBox(height: 8),
Text(
daysUntilNextPeriod > 0
? '$daysUntilNextPeriod days until period'
: 'Period expected',
style: GoogleFonts.outfit(
fontSize: 12,
color: AppColors.warmGray,
=======
State<CycleRing> createState() => _CycleRingState();
}
class _CycleRingState extends State<CycleRing> with SingleTickerProviderStateMixin {
class _CycleRingState extends State<CycleRing>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@@ -125,7 +55,7 @@ class _CycleRingState extends State<CycleRing> with SingleTickerProviderStateMix
animation: _animation,
builder: (context, child) {
final currentProgress = targetProgress * _animation.value;
return SizedBox(
width: 220,
height: 220,
@@ -141,7 +71,7 @@ class _CycleRingState extends State<CycleRing> with SingleTickerProviderStateMix
isDark: isDark,
),
),
// Center content with scale and fade animation
Transform.scale(
scale: 0.8 + (0.2 * _animation.value),
@@ -152,19 +82,28 @@ class _CycleRingState extends State<CycleRing> with SingleTickerProviderStateMix
children: [
Text(
'Day ${widget.dayOfCycle}',
style: Theme.of(context).textTheme.displayMedium?.copyWith(
fontSize: 32,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
style: Theme.of(context)
.textTheme
.displayMedium
?.copyWith(
fontSize: 32,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getPhaseColor(widget.phase).withOpacity(isDark ? 0.3 : 0.2),
color: _getPhaseColor(widget.phase)
.withOpacity(isDark ? 0.3 : 0.2),
borderRadius: BorderRadius.circular(20),
border: isDark ? Border.all(color: _getPhaseColor(widget.phase).withOpacity(0.5)) : null,
border: isDark
? Border.all(
color: _getPhaseColor(widget.phase)
.withOpacity(0.5))
: null,
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -179,7 +118,9 @@ class _CycleRingState extends State<CycleRing> with SingleTickerProviderStateMix
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: isDark ? Colors.white : _getPhaseColor(widget.phase),
color: isDark
? Colors.white
: _getPhaseColor(widget.phase),
),
),
],
@@ -191,24 +132,19 @@ class _CycleRingState extends State<CycleRing> with SingleTickerProviderStateMix
? '$daysUntilNextPeriod days until period'
: 'Period expected',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
fontSize: 12,
color:
Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
>>>>>>> 6742220 (Your commit message here)
),
),
],
),
<<<<<<< HEAD
],
),
=======
);
},
>>>>>>> 6742220 (Your commit message here)
);
}
@@ -229,18 +165,13 @@ class _CycleRingState extends State<CycleRing> with SingleTickerProviderStateMix
class _CycleRingPainter extends CustomPainter {
final double progress;
final CyclePhase phase;
<<<<<<< HEAD
_CycleRingPainter({required this.progress, required this.phase});
=======
final bool isDark;
_CycleRingPainter({
required this.progress,
required this.progress,
required this.phase,
required this.isDark,
});
>>>>>>> 6742220 (Your commit message here)
@override
void paint(Canvas canvas, Size size) {
@@ -250,11 +181,8 @@ class _CycleRingPainter extends CustomPainter {
// Background arc
final bgPaint = Paint()
<<<<<<< HEAD
..color = AppColors.lightGray.withOpacity(0.2)
=======
..color = (isDark ? Colors.white : AppColors.lightGray).withOpacity(isDark ? 0.05 : 0.1)
>>>>>>> 6742220 (Your commit message here)
..color =
(isDark ? Colors.white : AppColors.lightGray).withOpacity(isDark ? 0.05 : 0.1)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
@@ -287,11 +215,7 @@ class _CycleRingPainter extends CustomPainter {
final dotY = center.dy + radius * math.sin(dotAngle);
final dotPaint = Paint()
<<<<<<< HEAD
..color = Colors.white
=======
..color = isDark ? const Color(0xFF1E1E1E) : Colors.white
>>>>>>> 6742220 (Your commit message here)
..style = PaintingStyle.fill;
final dotBorderPaint = Paint()
@@ -308,11 +232,19 @@ class _CycleRingPainter extends CustomPainter {
case CyclePhase.menstrual:
return [AppColors.rose, AppColors.menstrualPhase, AppColors.blushPink];
case CyclePhase.follicular:
return [AppColors.sageGreen, AppColors.follicularPhase, AppColors.sageGreen.withOpacity(0.7)];
return [
AppColors.sageGreen,
AppColors.follicularPhase,
AppColors.sageGreen.withOpacity(0.7)
];
case CyclePhase.ovulation:
return [AppColors.lavender, AppColors.ovulationPhase, AppColors.rose];
case CyclePhase.luteal:
return [AppColors.lutealPhase, AppColors.lavender, AppColors.blushPink];
return [
AppColors.lutealPhase,
AppColors.lavender,
AppColors.blushPink
];
}
}

View File

@@ -1,43 +1,4 @@
import 'package:flutter/material.dart';
<<<<<<< HEAD
import 'package:google_fonts/google_fonts.dart';
import '../theme/app_theme.dart';
import '../screens/log/log_screen.dart';
class QuickLogButtons extends StatelessWidget {
const QuickLogButtons({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
_buildQuickButton(
icon: Icons.water_drop_outlined,
label: 'Period',
color: AppColors.menstrualPhase,
onTap: () => _navigateToLog(context),
),
const SizedBox(width: 12),
_buildQuickButton(
icon: Icons.emoji_emotions_outlined,
label: 'Mood',
color: AppColors.softGold,
onTap: () => _navigateToLog(context),
),
const SizedBox(width: 12),
_buildQuickButton(
icon: Icons.flash_on_outlined,
label: 'Energy',
color: AppColors.follicularPhase,
onTap: () => _navigateToLog(context),
),
const SizedBox(width: 12),
_buildQuickButton(
icon: Icons.healing_outlined,
label: 'Symptoms',
color: AppColors.lavender,
onTap: () => _navigateToLog(context),
=======
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import '../theme/app_theme.dart';
@@ -80,32 +41,11 @@ class QuickLogButtons extends ConsumerWidget {
label: 'Symptoms',
color: AppColors.lavender,
onTap: () => _navigateToLog(ref),
>>>>>>> 6742220 (Your commit message here)
),
],
);
}
<<<<<<< HEAD
void _navigateToLog(BuildContext context) {
// Navigate to the Log tab (index 2) of HomeScreen if possible,
// but since we are inside a tab, we can't easily switch the parent tab index without context. Using a provider or callback would be best.
// For now, let's push the LogScreen as a new route for "Quick Log" feel.
// Ideally we would switch the BottomNavBar index.
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(0),
child: SizedBox.shrink()
),
body: LogScreen()
)),
);
}
Widget _buildQuickButton({
=======
void _navigateToLog(WidgetRef ref) {
// Navigate to the Log tab (index 2)
ref.read(navigationProvider.notifier).setIndex(2);
@@ -113,39 +53,13 @@ class QuickLogButtons extends ConsumerWidget {
Widget _buildQuickButton(
BuildContext context, {
>>>>>>> 6742220 (Your commit message here)
required IconData icon,
required String label,
required Color color,
required VoidCallback onTap,
}) {
<<<<<<< HEAD
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 24),
const SizedBox(height: 6),
Text(
label,
style: GoogleFonts.outfit(
fontSize: 11,
fontWeight: FontWeight.w500,
color: color,
),
),
],
=======
final isDark = Theme.of(context).brightness == Brightness.dark;
return Expanded(
child: Material(
color: Colors.transparent,
@@ -157,7 +71,8 @@ class QuickLogButtons extends ConsumerWidget {
decoration: BoxDecoration(
color: color.withOpacity(isDark ? 0.2 : 0.15),
borderRadius: BorderRadius.circular(12),
border: isDark ? Border.all(color: color.withOpacity(0.3)) : null,
border:
isDark ? Border.all(color: color.withOpacity(0.3)) : null,
),
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -174,7 +89,6 @@ class QuickLogButtons extends ConsumerWidget {
),
],
),
>>>>>>> 6742220 (Your commit message here)
),
),
),

View File

@@ -6,122 +6,45 @@ import '../models/cycle_entry.dart';
class ScriptureCard extends StatelessWidget {
final String verse;
final String reference;
<<<<<<< HEAD
final CyclePhase phase;
=======
final String? translation;
final CyclePhase phase;
final VoidCallback? onTranslationTap;
>>>>>>> 6742220 (Your commit message here)
const ScriptureCard({
super.key,
required this.verse,
required this.reference,
<<<<<<< HEAD
required this.phase,
=======
this.translation,
required this.phase,
this.onTranslationTap,
>>>>>>> 6742220 (Your commit message here)
});
@override
Widget build(BuildContext context) {
<<<<<<< HEAD
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: _getGradientColors(phase),
=======
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: _getGradientColors(context, phase),
>>>>>>> 6742220 (Your commit message here)
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
<<<<<<< HEAD
boxShadow: [
BoxShadow(
color: _getPhaseColor(phase).withOpacity(0.2),
=======
border: Border.all(color: isDark ? Colors.white.withOpacity(0.05) : Colors.black.withOpacity(0.05)),
border: Border.all(
color: isDark
? Colors.white.withOpacity(0.05)
: Colors.black.withOpacity(0.05)),
boxShadow: [
BoxShadow(
color: _getPhaseColor(phase).withOpacity(isDark ? 0.05 : 0.15),
>>>>>>> 6742220 (Your commit message here)
blurRadius: 15,
offset: const Offset(0, 8),
),
],
),
<<<<<<< HEAD
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Scripture icon
Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.menu_book_outlined,
size: 18,
color: AppColors.charcoal.withOpacity(0.8),
),
),
const SizedBox(width: 8),
Text(
'Today\'s Verse',
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.charcoal.withOpacity(0.7),
letterSpacing: 0.5,
),
),
],
),
const SizedBox(height: 16),
// Verse
Text(
'"$verse"',
style: GoogleFonts.lora(
fontSize: 16,
fontStyle: FontStyle.italic,
color: AppColors.charcoal,
height: 1.6,
),
),
const SizedBox(height: 12),
// Reference
Text(
'$reference',
style: GoogleFonts.outfit(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
],
=======
child: Material(
color: Colors.transparent,
child: Padding(
@@ -136,13 +59,16 @@ class ScriptureCard extends StatelessWidget {
width: 32,
height: 32,
decoration: BoxDecoration(
color: (isDark ? Colors.white : Colors.black).withOpacity(0.1),
color: (isDark ? Colors.white : Colors.black)
.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.menu_book_outlined,
size: 18,
color: isDark ? Colors.white70 : AppColors.charcoal.withOpacity(0.8),
color: isDark
? Colors.white70
: AppColors.charcoal.withOpacity(0.8),
),
),
const SizedBox(width: 8),
@@ -150,40 +76,46 @@ class ScriptureCard extends StatelessWidget {
'Today\'s Verse',
style: theme.textTheme.labelLarge?.copyWith(
fontSize: 12,
color: isDark ? Colors.white60 : AppColors.charcoal.withOpacity(0.7),
color: isDark
? Colors.white60
: AppColors.charcoal.withOpacity(0.7),
letterSpacing: 0.5,
),
),
],
),
const SizedBox(height: 16),
// Verse
Text(
'"$verse"',
style: scriptureStyle(context, fontSize: 17),
),
const SizedBox(height: 12),
// Reference & Translation
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'$reference',
style: scriptureRefStyle(context).copyWith(fontSize: 13, fontWeight: FontWeight.w600),
style: scriptureRefStyle(context)
.copyWith(fontSize: 13, fontWeight: FontWeight.w600),
),
if (translation != null)
InkWell(
onTap: onTranslationTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: (isDark ? Colors.white : Colors.black).withOpacity(0.05),
color: (isDark ? Colors.white : Colors.black)
.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: (isDark ? Colors.white : Colors.black).withOpacity(0.1),
color: (isDark ? Colors.white : Colors.black)
.withOpacity(0.1),
),
),
child: Row(
@@ -199,9 +131,11 @@ class ScriptureCard extends StatelessWidget {
),
const SizedBox(width: 4),
Icon(
Icons.swap_horiz,
size: 14,
color: isDark ? Colors.white38 : AppColors.warmGray,
Icons.swap_horiz,
size: 14,
color: isDark
? Colors.white38
: AppColors.warmGray,
),
],
),
@@ -212,38 +146,14 @@ class ScriptureCard extends StatelessWidget {
],
),
),
>>>>>>> 6742220 (Your commit message here)
),
);
}
<<<<<<< HEAD
List<Color> _getGradientColors(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return [
AppColors.blushPink.withOpacity(0.6),
AppColors.cream,
];
case CyclePhase.follicular:
return [
AppColors.sageGreen.withOpacity(0.3),
AppColors.cream,
];
case CyclePhase.ovulation:
return [
AppColors.lavender.withOpacity(0.5),
AppColors.cream,
];
case CyclePhase.luteal:
return [
AppColors.lutealPhase.withOpacity(0.3),
AppColors.cream,
=======
List<Color> _getGradientColors(BuildContext context, CyclePhase phase) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final baseColor = isDark ? const Color(0xFF1E1E1E) : AppColors.cream;
switch (phase) {
case CyclePhase.menstrual:
return [
@@ -264,7 +174,6 @@ class ScriptureCard extends StatelessWidget {
return [
AppColors.lutealPhase.withOpacity(isDark ? 0.15 : 0.3),
baseColor,
>>>>>>> 6742220 (Your commit message here)
];
}
}

View File

@@ -15,31 +15,22 @@ class TipCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
<<<<<<< HEAD
=======
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
>>>>>>> 6742220 (Your commit message here)
final tip = _getTipForPhase(phase, isMarried);
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
<<<<<<< HEAD
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.05),
=======
color: theme.cardColor,
borderRadius: BorderRadius.circular(16),
border: isDark ? Border.all(color: Colors.white.withOpacity(0.05)) : null,
border:
isDark ? Border.all(color: Colors.white.withOpacity(0.05)) : null,
boxShadow: [
BoxShadow(
color: (isDark ? Colors.black : AppColors.charcoal).withOpacity(0.05),
>>>>>>> 6742220 (Your commit message here)
color: (isDark ? Colors.black : AppColors.charcoal)
.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
@@ -52,11 +43,7 @@ class TipCard extends StatelessWidget {
width: 40,
height: 40,
decoration: BoxDecoration(
<<<<<<< HEAD
color: AppColors.sageGreen.withOpacity(0.15),
=======
color: AppColors.sageGreen.withOpacity(isDark ? 0.2 : 0.15),
>>>>>>> 6742220 (Your commit message here)
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
@@ -72,29 +59,14 @@ class TipCard extends StatelessWidget {
children: [
Text(
'Today\'s Tip',
<<<<<<< HEAD
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
=======
style: theme.textTheme.titleMedium?.copyWith(
fontSize: 14,
fontWeight: FontWeight.w600,
>>>>>>> 6742220 (Your commit message here)
),
),
const SizedBox(height: 4),
Text(
tip,
<<<<<<< HEAD
style: GoogleFonts.outfit(
fontSize: 13,
color: AppColors.warmGray,
height: 1.4,
),
),
=======
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 13,
height: 1.4,
@@ -122,7 +94,6 @@ class TipCard extends StatelessWidget {
],
),
),
>>>>>>> 6742220 (Your commit message here)
],
),
),
@@ -131,21 +102,6 @@ class TipCard extends StatelessWidget {
);
}
<<<<<<< HEAD
String _getTipForPhase(CyclePhase phase, bool isMarried) {
switch (phase) {
case CyclePhase.menstrual:
return 'This is a time for rest. Honor your body with extra sleep, warm drinks, and gentle movement. God designed your body with wisdom.';
case CyclePhase.follicular:
return 'Your energy is rising! This is a great time to start new projects, exercise more intensely, and spend time in community.';
case CyclePhase.ovulation:
if (isMarried) {
return 'This is your most fertile window. You may feel more social and energetic. Prioritize connection with your spouse.';
}
return 'You may feel more social and confident during this phase. It\'s a great time for important conversations and presentations.';
case CyclePhase.luteal:
return 'As you enter the luteal phase, focus on nourishing foods, adequate sleep, and stress management. Be gentle with yourself.';
=======
void _showDetailedInsights(BuildContext context) {
final details = _getDetailsForPhase(phase);
showDialog(
@@ -170,9 +126,11 @@ class TipCard extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailSection('Nutrition', details['nutrition']!, Icons.restaurant),
_buildDetailSection(
'Nutrition', details['nutrition']!, Icons.restaurant),
const SizedBox(height: 16),
_buildDetailSection('Movement', details['movement']!, Icons.fitness_center),
_buildDetailSection(
'Movement', details['movement']!, Icons.fitness_center),
const SizedBox(height: 16),
Text(
'Note: While these are general trends, your body is unique. Always listen to your own energy levels.',
@@ -187,7 +145,8 @@ class TipCard extends StatelessWidget {
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Got it', style: GoogleFonts.outfit(color: AppColors.sageGreen)),
child: Text('Got it',
style: GoogleFonts.outfit(color: AppColors.sageGreen)),
),
],
),
@@ -243,25 +202,32 @@ class TipCard extends StatelessWidget {
switch (phase) {
case CyclePhase.menstrual:
return {
'nutrition': 'Incorporate leafy greens, red meat or lentils for iron. Pair with citrus for better absorption.',
'movement': 'Gentle walking, restorative yoga, or just deep breathing. Avoid high-intensity stress.',
'nutrition':
'Incorporate leafy greens, red meat or lentils for iron. Pair with citrus for better absorption.',
'movement':
'Gentle walking, restorative yoga, or just deep breathing. Avoid high-intensity stress.',
};
case CyclePhase.follicular:
return {
'nutrition': 'Broccoli, cauliflower, and fermented foods help balance rising estrogen. Focus on lean proteins.',
'movement': 'Strength training and steady cardio. Your body is primed for building and renewal.',
'nutrition':
'Broccoli, cauliflower, and fermented foods help balance rising estrogen. Focus on lean proteins.',
'movement':
'Strength training and steady cardio. Your body is primed for building and renewal.',
};
case CyclePhase.ovulation:
return {
'nutrition': 'Avocados, nuts, and seeds provide healthy fats for peak hormonal health. Keep hydration high.',
'movement': 'Highest intensity workouts, HIIT, or group sports. You have peak stamina and strength right now.',
'nutrition':
'Avocados, nuts, and seeds provide healthy fats for peak hormonal health. Keep hydration high.',
'movement':
'Highest intensity workouts, HIIT, or group sports. You have peak stamina and strength right now.',
};
case CyclePhase.luteal:
return {
'nutrition': 'Dark chocolate (70%+), pumpkin seeds, and bananas for magnesium to help with cramps.',
'movement': 'Pilates, steady-state swimming, or hiking. Focus on persistence rather than peak intensity.',
'nutrition':
'Dark chocolate (70%+), pumpkin seeds, and bananas for magnesium to help with cramps.',
'movement':
'Pilates, steady-state swimming, or hiking. Focus on persistence rather than peak intensity.',
};
>>>>>>> 6742220 (Your commit message here)
}
}
}

View File

@@ -5,41 +5,18 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
<<<<<<< HEAD
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
url: "https://pub.dev"
source: hosted
version: "72.0.0"
=======
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
url: "https://pub.dev"
source: hosted
version: "76.0.0"
>>>>>>> 6742220 (Your commit message here)
_macros:
dependency: transitive
description: dart
source: sdk
<<<<<<< HEAD
version: "0.3.2"
=======
version: "0.3.3"
>>>>>>> 6742220 (Your commit message here)
version: "67.0.0"
analyzer:
dependency: transitive
dependency: "direct dev"
description:
name: analyzer
<<<<<<< HEAD
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
url: "https://pub.dev"
source: hosted
version: "6.7.0"
=======
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
url: "https://pub.dev"
source: hosted
version: "6.11.0"
>>>>>>> 6742220 (Your commit message here)
version: "6.4.1"
args:
dependency: transitive
description:
@@ -76,18 +53,18 @@ packages:
dependency: transitive
description:
name: build_config
sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9"
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
version: "4.0.4"
build_resolvers:
dependency: transitive
description:
@@ -132,10 +109,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.0"
checked_yaml:
dependency: transitive
description:
@@ -148,10 +125,10 @@ packages:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
code_builder:
dependency: transitive
description:
@@ -164,17 +141,10 @@ packages:
dependency: transitive
description:
name: collection
<<<<<<< HEAD
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
=======
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev"
source: hosted
version: "1.19.0"
>>>>>>> 6742220 (Your commit message here)
version: "1.19.1"
convert:
dependency: transitive
description:
@@ -203,10 +173,10 @@ packages:
dependency: transitive
description:
name: dart_style
sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab"
sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
url: "https://pub.dev"
source: hosted
version: "2.3.7"
version: "2.3.6"
dbus:
dependency: transitive
description:
@@ -227,10 +197,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.3.3"
ffi:
dependency: transitive
description:
@@ -312,10 +282,10 @@ packages:
dependency: "direct main"
description:
name: flutter_svg
sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1
sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.2.0"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -402,10 +372,10 @@ packages:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
version: "4.1.2"
intl:
dependency: "direct main"
description:
@@ -442,40 +412,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
<<<<<<< HEAD
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "10.0.5"
=======
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.dev"
source: hosted
version: "10.0.7"
>>>>>>> 6742220 (Your commit message here)
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
<<<<<<< HEAD
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
=======
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.dev"
source: hosted
version: "3.0.8"
>>>>>>> 6742220 (Your commit message here)
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
lints:
dependency: transitive
description:
@@ -492,29 +448,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
macros:
dependency: transitive
description:
name: macros
<<<<<<< HEAD
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
=======
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
version: "0.1.3-main.0"
>>>>>>> 6742220 (Your commit message here)
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
@@ -527,10 +468,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.15.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -539,6 +480,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
mockito:
dependency: "direct dev"
description:
name: mockito
sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917"
url: "https://pub.dev"
source: hosted
version: "5.4.4"
package_config:
dependency: transitive
description:
@@ -551,10 +500,10 @@ packages:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
version: "1.9.1"
path_parsing:
dependency: transitive
description:
@@ -575,10 +524,10 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
url: "https://pub.dev"
source: hosted
version: "2.2.15"
version: "2.2.17"
path_provider_foundation:
dependency: transitive
description:
@@ -663,10 +612,10 @@ packages:
dependency: transitive
description:
name: pubspec_parse
sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0"
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.5.0"
riverpod:
dependency: transitive
description:
@@ -687,10 +636,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: "9f9f3d372d4304723e6136663bb291c0b93f5e4c8a4a6314347f481a33bda2b1"
sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e"
url: "https://pub.dev"
source: hosted
version: "2.4.7"
version: "2.4.11"
shared_preferences_foundation:
dependency: transitive
description:
@@ -735,10 +684,10 @@ packages:
dependency: transitive
description:
name: shelf
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.1"
version: "1.4.2"
shelf_web_socket:
dependency: transitive
description:
@@ -759,11 +708,7 @@ packages:
dependency: transitive
description: flutter
source: sdk
<<<<<<< HEAD
version: "0.0.99"
=======
version: "0.0.0"
>>>>>>> 6742220 (Your commit message here)
smooth_page_indicator:
dependency: "direct main"
description:
@@ -800,17 +745,10 @@ packages:
dependency: transitive
description:
name: stack_trace
<<<<<<< HEAD
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
=======
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
>>>>>>> 6742220 (Your commit message here)
version: "1.12.1"
state_notifier:
dependency: transitive
description:
@@ -823,10 +761,10 @@ packages:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.4"
stream_transform:
dependency: transitive
description:
@@ -839,17 +777,10 @@ packages:
dependency: transitive
description:
name: string_scanner
<<<<<<< HEAD
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
=======
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
>>>>>>> 6742220 (Your commit message here)
table_calendar:
dependency: "direct main"
description:
@@ -870,17 +801,10 @@ packages:
dependency: transitive
description:
name: test_api
<<<<<<< HEAD
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
version: "0.7.2"
=======
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.dev"
source: hosted
version: "0.7.3"
>>>>>>> 6742220 (Your commit message here)
version: "0.7.7"
timezone:
dependency: transitive
description:
@@ -917,10 +841,10 @@ packages:
dependency: transitive
description:
name: vector_graphics
sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de"
sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6
url: "https://pub.dev"
source: hosted
version: "1.1.18"
version: "1.1.19"
vector_graphics_codec:
dependency: transitive
description:
@@ -933,33 +857,26 @@ packages:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad"
sha256: ca81fdfaf62a5ab45d7296614aea108d2c7d0efca8393e96174bf4d51e6725b0
url: "https://pub.dev"
source: hosted
version: "1.1.16"
version: "1.1.18"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
<<<<<<< HEAD
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev"
source: hosted
version: "14.2.5"
=======
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.dev"
source: hosted
version: "14.3.0"
>>>>>>> 6742220 (Your commit message here)
watcher:
dependency: transitive
description:
@@ -1001,7 +918,7 @@ packages:
source: hosted
version: "1.1.0"
xml:
dependency: transitive
dependency: "direct main"
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
@@ -1017,5 +934,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"
dart: ">=3.8.0-0 <4.0.0"
flutter: ">=3.27.0"

View File

@@ -37,6 +37,7 @@ dependencies:
# Utils
uuid: ^4.5.1
shared_preferences: ^2.3.2
xml: ^6.3.0 # Added for XML parsing
dev_dependencies:
flutter_test:
@@ -44,9 +45,13 @@ dev_dependencies:
flutter_lints: ^4.0.0
hive_generator: ^2.0.1
build_runner: ^2.4.7
mockito: ^5.0.0 # Added for testing
analyzer: ^6.4.1 # Downgraded to resolve dependency conflict with hive_generator
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/scriptures.json
- assets/scriptures_optimized.json
- assets/bible_xml/

View File

@@ -0,0 +1,40 @@
import 'package:christian_period_tracker/models/cycle_entry.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('CycleEntry', () {
test('hasSymptoms returns true when there are symptoms', () {
final entry = CycleEntry(
id: '1',
date: DateTime.now(),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
hasHeadache: true,
);
expect(entry.hasSymptoms, isTrue);
});
test('hasSymptoms returns false when there are no symptoms', () {
final entry = CycleEntry(
id: '1',
date: DateTime.now(),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
expect(entry.hasSymptoms, isFalse);
});
test('symptomCount returns the correct number of symptoms', () {
final entry = CycleEntry(
id: '1',
date: DateTime.now(),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
hasHeadache: true,
hasBloating: true,
crampIntensity: 2,
);
expect(entry.symptomCount, 3);
});
});
}

View File

@@ -7,8 +7,8 @@ void main() {
group('CycleService Tests', () {
test('calculateCycleInfo returns follicular phase for null profile', () {
final info = CycleService.calculateCycleInfo(null);
expect(info['phase'], CyclePhase.follicular);
expect(info['dayOfCycle'], 1);
expect(info.phase, CyclePhase.follicular);
expect(info.dayOfCycle, 1);
});
test('calculateCycleInfo calculates follicular phase correctly (Day 7)', () {
@@ -26,8 +26,8 @@ void main() {
);
final info = CycleService.calculateCycleInfo(user);
expect(info['dayOfCycle'], 7);
expect(info['phase'], CyclePhase.follicular);
expect(info.dayOfCycle, 7);
expect(info.phase, CyclePhase.follicular);
});
test('calculateCycleInfo calculates menstrual phase correctly (Day 2)', () {
@@ -45,8 +45,8 @@ void main() {
);
final info = CycleService.calculateCycleInfo(user);
expect(info['dayOfCycle'], 2);
expect(info['phase'], CyclePhase.menstrual);
expect(info.dayOfCycle, 2);
expect(info.phase, CyclePhase.menstrual);
});
test('calculateCycleInfo handles cycle length wrapping', () {
@@ -65,8 +65,8 @@ void main() {
);
final info = CycleService.calculateCycleInfo(user);
expect(info['dayOfCycle'], 3);
expect(info['phase'], CyclePhase.menstrual);
expect(info.dayOfCycle, 3);
expect(info.phase, CyclePhase.menstrual);
});
});
}

5
test/mocks.dart Normal file
View File

@@ -0,0 +1,5 @@
import 'package:mockito/annotations.dart';
import 'package:christian_period_tracker/services/bible_xml_parser.dart';
@GenerateMocks([BibleXmlParser])
void main() {} // Dummy main function to satisfy Dart

91
test/mocks.mocks.dart Normal file
View File

@@ -0,0 +1,91 @@
// Mocks generated by Mockito 5.4.4 from annotations
// in christian_period_tracker/test/mocks.dart.
// Do not manually edit this file.
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i4;
import 'package:christian_period_tracker/services/bible_xml_parser.dart' as _i3;
import 'package:mockito/mockito.dart' as _i1;
import 'package:xml/xml.dart' as _i2;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
// ignore_for_file: avoid_setters_without_getters
// ignore_for_file: comment_references
// ignore_for_file: deprecated_member_use
// ignore_for_file: deprecated_member_use_from_same_package
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
// ignore_for_file: subtype_of_sealed_class
class _FakeXmlDocument_0 extends _i1.SmartFake implements _i2.XmlDocument {
_FakeXmlDocument_0(
Object parent,
Invocation parentInvocation,
) : super(
parent,
parentInvocation,
);
}
/// A class which mocks [BibleXmlParser].
///
/// See the documentation for Mockito's code generation for more information.
class MockBibleXmlParser extends _i1.Mock implements _i3.BibleXmlParser {
MockBibleXmlParser() {
_i1.throwOnMissingStub(this);
}
@override
_i4.Future<_i2.XmlDocument> loadXmlAsset(String? assetPath) =>
(super.noSuchMethod(
Invocation.method(
#loadXmlAsset,
[assetPath],
),
returnValue: _i4.Future<_i2.XmlDocument>.value(_FakeXmlDocument_0(
this,
Invocation.method(
#loadXmlAsset,
[assetPath],
),
)),
) as _i4.Future<_i2.XmlDocument>);
@override
String? getVerseFromXml(
_i2.XmlDocument? document,
String? bookName,
int? chapterNum,
int? verseNum,
) =>
(super.noSuchMethod(Invocation.method(
#getVerseFromXml,
[
document,
bookName,
chapterNum,
verseNum,
],
)) as String?);
@override
_i4.Future<String?> getVerseFromAsset(
String? assetPath,
String? reference,
) =>
(super.noSuchMethod(
Invocation.method(
#getVerseFromAsset,
[
assetPath,
reference,
],
),
returnValue: _i4.Future<String?>.value(),
) as _i4.Future<String?>);
}

View File

@@ -0,0 +1,262 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mockito/mockito.dart';
import 'package:christian_period_tracker/models/scripture.dart';
import 'package:christian_period_tracker/models/cycle_entry.dart';
import 'package:christian_period_tracker/models/user_profile.dart';
import 'package:christian_period_tracker/providers/scripture_provider.dart';
import 'package:christian_period_tracker/providers/user_provider.dart';
import 'package:hive_flutter/hive_flutter.dart';
// Fake ScriptureDatabase implementation for testing
class FakeScriptureDatabase implements ScriptureDatabase {
final int Function(String phase) getScriptureCountForPhaseFn;
final Scripture? Function(String phase, int index) getScriptureForPhaseByIndexFn;
final Scripture? Function(String phase)? getRandomScriptureForPhaseFn;
FakeScriptureDatabase({
required this.getScriptureCountForPhaseFn,
required this.getScriptureForPhaseByIndexFn,
this.getRandomScriptureForPhaseFn,
});
@override
int getScriptureCountForPhase(String phase) => getScriptureCountForPhaseFn(phase);
@override
Scripture? getScriptureForPhaseByIndex(String phase, int index) =>
getScriptureForPhaseByIndexFn(phase, index);
@override
Scripture? getRandomScriptureForPhase(String phase) =>
getRandomScriptureForPhaseFn != null ? getRandomScriptureForPhaseFn!(phase) : null;
// Unimplemented methods (not used by ScriptureNotifier)
@override
List<Scripture> getAllScriptures() => throw UnimplementedError();
@override
Scripture getHusbandScripture() => throw UnimplementedError();
@override
Future<void> loadScriptures() => Future.value(); // Can be mocked to do nothing
@override
Scripture? getRecommendedScripture(CycleEntry entry) => throw UnimplementedError();
@override
Scripture getScriptureForPhase(String phase) => throw UnimplementedError();
}
void main() {
group('ScriptureNotifier', () {
late ProviderContainer container;
late String testPath;
setUpAll(() async {
testPath = Directory.current.path + '/test_hive_temp_scripture_provider';
final Directory tempDir = Directory(testPath);
if (!await tempDir.exists()) {
await tempDir.create(recursive: true);
}
Hive.init(testPath);
Hive.registerAdapter(UserProfileAdapter());
Hive.registerAdapter(RelationshipStatusAdapter());
Hive.registerAdapter(FertilityGoalAdapter());
Hive.registerAdapter(BibleTranslationAdapter());
Hive.registerAdapter(UserRoleAdapter());
await Hive.openBox<UserProfile>('user_profile');
});
tearDownAll(() async {
await Hive.close();
await Directory(testPath).delete(recursive: true);
});
final testScripture1 = Scripture(
verses: {BibleTranslation.esv: "Verse 1"},
reference: "Ref 1",
applicablePhases: ['menstrual'],
);
final testScripture2 = Scripture(
verses: {BibleTranslation.esv: "Verse 2"},
reference: "Ref 2",
applicablePhases: ['menstrual'],
);
final testScripture3 = Scripture(
verses: {BibleTranslation.esv: "Verse 3"},
reference: "Ref 3",
applicablePhases: ['menstrual'],
);
tearDown(() {
container.dispose();
});
test('initializes with correct scripture for phase', () async {
final fakeDb = FakeScriptureDatabase(
getScriptureCountForPhaseFn: (phase) => 3,
getScriptureForPhaseByIndexFn: (phase, index) => testScripture1,
);
container = ProviderContainer(
overrides: [
scriptureDatabaseProvider.overrideWithValue(fakeDb),
userProfileProvider.overrideWith(
(ref) => UserProfileNotifier(),
),
],
);
final notifier = container.read(scriptureProvider.notifier);
notifier.initializeScripture(CyclePhase.menstrual);
final state = container.read(scriptureProvider);
expect(state.currentScripture, testScripture1);
expect(state.currentPhase, CyclePhase.menstrual);
expect(state.maxIndex, 3);
// currentIndex will depend on dayOfYear % 3, which is hard to predict
// So we'll just check it's within bounds.
expect(state.currentIndex, isNonNegative);
expect(state.currentIndex, lessThan(3));
});
test('getNextScripture cycles correctly', () async {
final scriptures = [testScripture1, testScripture2, testScripture3];
final fakeDb = FakeScriptureDatabase(
getScriptureCountForPhaseFn: (phase) => scriptures.length,
getScriptureForPhaseByIndexFn: (phase, index) => scriptures[index],
);
container = ProviderContainer(
overrides: [
scriptureDatabaseProvider.overrideWithValue(fakeDb),
userProfileProvider.overrideWith(
(ref) => UserProfileNotifier(),
),
],
);
final notifier = container.read(scriptureProvider.notifier);
// Force initial state to 0 for predictable cycling
notifier.initializeScripture(CyclePhase.menstrual);
// Override currentIndex to 0 for predictable test
container.read(scriptureProvider.notifier).state = container.read(scriptureProvider).copyWith(currentIndex: 0);
// First next
notifier.getNextScripture();
expect(container.read(scriptureProvider).currentScripture, testScripture2);
expect(container.read(scriptureProvider).currentIndex, 1);
// Second next
notifier.getNextScripture();
expect(container.read(scriptureProvider).currentScripture, testScripture3);
expect(container.read(scriptureProvider).currentIndex, 2);
// Wrap around
notifier.getNextScripture();
expect(container.read(scriptureProvider).currentScripture, testScripture1);
expect(container.read(scriptureProvider).currentIndex, 0);
});
test('getPreviousScripture cycles correctly', () async {
final scriptures = [testScripture1, testScripture2, testScripture3];
final fakeDb = FakeScriptureDatabase(
getScriptureCountForPhaseFn: (phase) => scriptures.length,
getScriptureForPhaseByIndexFn: (phase, index) => scriptures[index],
);
container = ProviderContainer(
overrides: [
scriptureDatabaseProvider.overrideWithValue(fakeDb),
userProfileProvider.overrideWith(
(ref) => UserProfileNotifier(),
),
],
);
final notifier = container.read(scriptureProvider.notifier);
// Force initial state to 0 for predictable cycling
notifier.initializeScripture(CyclePhase.menstrual);
// Override currentIndex to 0 for predictable test
container.read(scriptureProvider.notifier).state = container.read(scriptureProvider).copyWith(currentIndex: 0);
// First previous (wraps around)
notifier.getPreviousScripture();
expect(container.read(scriptureProvider).currentScripture, testScripture3);
expect(container.read(scriptureProvider).currentIndex, 2);
// Second previous
notifier.getPreviousScripture();
expect(container.read(scriptureProvider).currentScripture, testScripture2);
expect(container.read(scriptureProvider).currentIndex, 1);
// Third previous
notifier.getPreviousScripture();
expect(container.read(scriptureProvider).currentScripture, testScripture1);
expect(container.read(scriptureProvider).currentIndex, 0);
});
test('getRandomScripture updates to a valid scripture', () async {
final scriptures = [testScripture1, testScripture2, testScripture3];
final fakeDb = FakeScriptureDatabase(
getScriptureCountForPhaseFn: (phase) => scriptures.length,
getScriptureForPhaseByIndexFn: (phase, index) => scriptures[index % scriptures.length], // Ensure it always returns a valid one
);
container = ProviderContainer(
overrides: [
scriptureDatabaseProvider.overrideWithValue(fakeDb),
userProfileProvider.overrideWith(
(ref) => UserProfileNotifier(),
),
],
);
final notifier = container.read(scriptureProvider.notifier);
notifier.initializeScripture(CyclePhase.menstrual);
// Perform a random selection
notifier.getRandomScripture();
final state = container.read(scriptureProvider);
expect(state.currentScripture, isNotNull);
expect(state.currentScripture, isIn(scriptures)); // Ensure it's one of the valid scriptures
expect(state.currentIndex, isNonNegative);
expect(state.currentIndex, lessThan(scriptures.length));
});
test('does not change state if maxIndex is 0', () async {
final fakeDb = FakeScriptureDatabase(
getScriptureCountForPhaseFn: (phase) => 0,
getScriptureForPhaseByIndexFn: (phase, index) => null,
);
container = ProviderContainer(
overrides: [
scriptureDatabaseProvider.overrideWithValue(fakeDb),
userProfileProvider.overrideWith(
(ref) => UserProfileNotifier(),
),
],
);
final notifier = container.read(scriptureProvider.notifier);
notifier.initializeScripture(CyclePhase.menstrual);
final initialState = container.read(scriptureProvider);
expect(initialState.currentScripture, isNull);
expect(initialState.maxIndex, 0);
notifier.getNextScripture();
notifier.getPreviousScripture();
notifier.getRandomScripture();
// State should remain unchanged
expect(container.read(scriptureProvider), initialState);
});
});
}

155
test/scripture_test.dart Normal file
View File

@@ -0,0 +1,155 @@
import 'dart:convert';
import 'dart:io';
import 'package:christian_period_tracker/models/scripture.dart';
import 'package:christian_period_tracker/models/user_profile.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:path_provider_linux/path_provider_linux.dart';
import 'package:mockito/mockito.dart';
import 'mocks.mocks.dart'; // Generated mock file
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('Scripture', () {
test('fromJson creates a valid Scripture object', () {
final json = {
"verses": {
"esv": "Test verse",
},
"reference": "Test 1:1",
};
final scripture = Scripture.fromJson(json);
expect(scripture.reference, "Test 1:1");
expect(scripture.getVerse(BibleTranslation.esv), "Test verse");
});
});
group('ScriptureDatabase', () {
late ScriptureDatabase database;
late String testPath;
setUpAll(() async {
// Initialize path_provider_platform_interface for testing
// This is a common pattern for mocking platform-specific plugins in tests.
PathProviderPlatform.instance = _MockPathProviderPlatform();
testPath = Directory.current.path + '/test_hive_temp';
// Ensure the directory exists
final Directory tempDir = Directory(testPath);
if (!await tempDir.exists()) {
await tempDir.create(recursive: true);
}
// Create and configure the mock BibleXmlParser
final mockBibleXmlParser = MockBibleXmlParser();
when(mockBibleXmlParser.getVerseFromAsset(any, any)).thenAnswer((invocation) async {
final String reference = invocation.positionalArguments[1];
// Return a mock verse based on the reference for testing purposes
return 'Mock Verse for $reference';
});
// Mock the rootBundle for JSON assets (XML assets are now handled by mockBibleXmlParser)
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMessageHandler(
'flutter/assets',
(ByteData? message) async {
final String key = utf8.decode(message!.buffer.asUint8List());
if (key == 'assets/scriptures.json' || key == 'assets/scriptures_optimized.json') {
final json = {
"menstrual": [
{
"verses": {"esv": "Menstrual verse"},
"reference": "Menstrual 1:1",
"applicablePhases": ["menstrual"]
}
],
"follicular": [
{
"verses": {"esv": "Follicular verse"},
"reference": "Follicular 1:1",
"applicablePhases": ["follicular"]
}
],
"ovulation": [
{
"verses": {"esv": "Ovulation verse"},
"reference": "Ovulation 1:1",
"applicablePhases": ["ovulation"]
}
],
"luteal": [
{
"verses": {"esv": "Luteal verse"},
"reference": "Luteal 1:1",
"applicablePhases": ["luteal"]
}
],
"husband": [
{
"verses": {"esv": "Husband verse"},
"reference": "Husband 1:1",
"applicablePhases": ["husband"]
}
],
"womanhood": [
{
"verses": {"esv": "Womanhood verse"},
"reference": "Womanhood 1:1",
"applicableContexts": ["womanhood"]
}
],
"contextual": {
"pain": [
{
"verses": {"esv": "Pain verse"},
"reference": "Pain 1:1",
"applicableContexts": ["pain"]
}
]
}
};
return utf8.encode(jsonEncode(json)).buffer.asByteData();
}
return null; // Return null for other asset requests not explicitly mocked
},
);
Hive.init(testPath);
Hive.registerAdapter(ScriptureAdapter()); // Register your adapter
Hive.registerAdapter(BibleTranslationAdapter()); // Register BibleTranslationAdapter
database = ScriptureDatabase(bibleXmlParser: mockBibleXmlParser); // Instantiate with mock
await database.loadScriptures();
});
tearDownAll(() async {
await Hive.close();
await Directory(testPath).delete(recursive: true);
});
test('getScriptureForPhase returns the correct scripture', () {
final scripture = database.getScriptureForPhase('menstrual');
expect(scripture.reference, "Menstrual 1:1");
});
test('getHusbandScripture returns a husband scripture', () {
final scripture = database.getHusbandScripture();
expect(scripture.applicablePhases, contains('husband'));
});
});
}
class _MockPathProviderPlatform extends PathProviderPlatform {
@override
Future<String?> getApplicationSupportPath() async {
return Directory.current.path + '/test_hive_temp';
}
@override
Future<String?> getApplicationDocumentsPath() async {
return Directory.current.path + '/test_hive_temp';
}
}

231
tool/optimize_assets.dart Normal file
View File

@@ -0,0 +1,231 @@
import 'dart:convert';
import 'dart:io';
import 'package:xml/xml.dart';
// Copy of book abbreviations from BibleXmlParser
const Map<String, String> _bookAbbreviations = {
'genesis': 'Gen', 'exodus': 'Exod', 'leviticus': 'Lev', 'numbers': 'Num',
'deuteronomy': 'Deut', 'joshua': 'Josh', 'judges': 'Judg', 'ruth': 'Ruth',
'1 samuel': '1Sam', '2 samuel': '2Sam', '1 kings': '1Kgs', '2 kings': '2Kgs',
'1 chronicles': '1Chr', '2 chronicles': '2Chr', 'ezra': 'Ezra', 'nehemiah': 'Neh',
'esther': 'Esth', 'job': 'Job', 'psalm': 'Ps', 'proverbs': 'Prov',
'ecclesiastes': 'Eccl', 'song of solomon': 'Song', 'isaiah': 'Isa', 'jeremiah': 'Jer',
'lamentations': 'Lam', 'ezekiel': 'Ezek', 'daniel': 'Dan', 'hosea': 'Hos',
'joel': 'Joel', 'amos': 'Amos', 'obadiah': 'Obad', 'jonah': 'Jonah',
'micah': 'Mic', 'nahum': 'Nah', 'habakkuk': 'Hab', 'zephaniah': 'Zeph',
'haggai': 'Hag', 'zechariah': 'Zech', 'malachi': 'Mal',
'matthew': 'Matt', 'mark': 'Mark', 'luke': 'Luke', 'john': 'John',
'acts': 'Acts', 'romans': 'Rom', '1 corinthians': '1Cor', '2 corinthians': '2Cor',
'galatians': 'Gal', 'ephesians': 'Eph', 'philippians': 'Phil', 'colossians': 'Col',
'1 thessalonians': '1Thess', '2 thessalonians': '2Thess', '1 timothy': '1Tim',
'2 timothy': '2Tim', 'titus': 'Titus', 'philemon': 'Phlm', 'hebrews': 'Heb',
'james': 'Jas', '1 peter': '1Pet', '2 peter': '2Pet', '1 john': '1John',
'2 john': '2John', '3 john': '3John', 'jude': 'Jude', 'revelation': 'Rev',
};
// Map of translations to filenames
final Map<String, String> _translationFiles = {
'esv': 'assets/bible_xml/ESV.xml',
'niv': 'assets/bible_xml/NIV.xml',
'nkjv': 'assets/bible_xml/NKJV.xml',
'nlt': 'assets/bible_xml/NLT.xml',
'nasb': 'assets/bible_xml/NASB.xml',
'kjv': 'assets/bible_xml/KJV.xml',
'msg': 'assets/bible_xml/MSG.xml',
};
void main() async {
print('Starting asset optimization...');
// 1. Load the base JSON
final File jsonFile = File('assets/scriptures.json');
if (!jsonFile.existsSync()) {
print('Error: assets/scriptures.json not found.');
return;
}
final Map<String, dynamic> data = json.decode(jsonFile.readAsStringSync());
// 2. Load and Parse all XMLs
final Map<String, XmlDocument> xmlDocs = {};
for (var entry in _translationFiles.entries) {
final key = entry.key;
final path = entry.value;
final file = File(path);
if (file.existsSync()) {
print('Parsing $key from $path...');
try {
xmlDocs[key] = XmlDocument.parse(file.readAsStringSync());
} catch (e) {
print('Error parsing $path: $e');
}
} else {
print('Warning: $path not found.');
}
}
// 3. Process the JSON structure
for (var phase in ['menstrual', 'follicular', 'ovulation', 'luteal']) {
if (data[phase] != null) {
await _processList(data[phase] as List, xmlDocs);
}
}
if (data['husband'] != null) {
await _processList(data['husband'] as List, xmlDocs);
}
if (data['womanhood'] != null) {
await _processList(data['womanhood'] as List, xmlDocs);
}
if (data['contextual'] != null) {
final contextualMap = data['contextual'] as Map<String, dynamic>;
for (var key in contextualMap.keys) {
await _processList(contextualMap[key] as List, xmlDocs);
}
}
// 4. Write the optimized JSON
final outputFile = File('assets/scriptures_optimized.json');
outputFile.writeAsStringSync(json.encode(data)); // Minified
// outputFile.writeAsStringSync(const JsonEncoder.withIndent(' ').convert(data)); // Pretty print
print('Optimization complete. Wrote to assets/scriptures_optimized.json');
}
Future<void> _processList(List list, Map<String, XmlDocument> xmlDocs) async {
for (var item in list) {
final String reference = item['reference'];
Map<String, dynamic> verses = item['verses'] ?? {};
// Parse reference
final parts = _parseReference(reference);
if (parts == null) {
print('Skipping invalid reference: $reference');
continue;
}
// Look up for each translation
for (var entry in _translationFiles.entries) {
final transKey = entry.key; // esv, niv, etc.
// If already has text, skip (or overwrite? Let's overwrite to ensure consistency,
// but the original JSON had manual entries. The user wants to use the XMLs.
// Let's only fill if missing or if we want to enforce XML source.)
// Strategy: Fill if we have XML data.
if (xmlDocs.containsKey(transKey)) {
final text = _getVerseFromXml(
xmlDocs[transKey]!,
parts['book']!,
int.parse(parts['chapter']!),
int.parse(parts['verse']!)
);
if (text != null) {
verses[transKey] = text;
} else {
print('Warning: Could not find $reference in $transKey');
}
}
}
item['verses'] = verses;
}
}
Map<String, String>? _parseReference(String reference) {
final parts = reference.split(' ');
if (parts.length < 2) return null;
String book = parts.sublist(0, parts.length - 1).join(' ').toLowerCase();
String chapterVerse = parts.last;
final cvParts = chapterVerse.split(':');
if (cvParts.length < 2) return null; // Only handles simple Chapter:Verse for now
return {
'book': book,
'chapter': cvParts[0],
// Handle cases like "1-2" by just taking the first one?
// The current parser Logic only takes one verse number.
// If the reference is "Psalm 23:1-2", the XML parser expects a single integer.
// We should probably just parse the first verse or handle ranges?
// For this fix, let's just parse the start verse to prevent crashing on int.parse
'verse': cvParts[1].split('-')[0],
};
}
String? _getVerseFromXml(XmlDocument document, String bookName, int chapterNum, int verseNum) {
// Standardize book name for lookup
String lookupBookName = _bookAbbreviations[bookName.toLowerCase()] ?? bookName;
// Use root element to avoid full document search
XmlElement root = document.rootElement;
// -- Find Book --
// Try Schema 1: Direct child of root
var bookElement = root.findElements('BIBLEBOOK').firstWhere(
(element) {
final nameAttr = element.getAttribute('bname');
return nameAttr?.toLowerCase() == lookupBookName.toLowerCase() ||
nameAttr?.toLowerCase() == bookName.toLowerCase();
},
orElse: () => XmlElement(XmlName('notfound')),
);
// Try Schema 2 if not found
if (bookElement.name.local == 'notfound') {
bookElement = root.findElements('b').firstWhere(
(element) {
final nameAttr = element.getAttribute('n');
return nameAttr?.toLowerCase() == lookupBookName.toLowerCase() ||
nameAttr?.toLowerCase() == bookName.toLowerCase();
},
orElse: () => XmlElement(XmlName('notfound')),
);
}
if (bookElement.name.local == 'notfound') {
return null;
}
// -- Find Chapter --
// Try Schema 1: Direct child of book
var chapterElement = bookElement.findElements('CHAPTER').firstWhere(
(element) => element.getAttribute('cnumber') == chapterNum.toString(),
orElse: () => XmlElement(XmlName('notfound')),
);
// Try Schema 2 if not found
if (chapterElement.name.local == 'notfound') {
chapterElement = bookElement.findElements('c').firstWhere(
(element) => element.getAttribute('n') == chapterNum.toString(),
orElse: () => XmlElement(XmlName('notfound')),
);
}
if (chapterElement.name.local == 'notfound') {
return null;
}
// -- Find Verse --
// Try Schema 1: Direct child of chapter
var verseElement = chapterElement.findElements('VERS').firstWhere(
(element) => element.getAttribute('vnumber') == verseNum.toString(),
orElse: () => XmlElement(XmlName('notfound')),
);
// Try Schema 2 if not found
if (verseElement.name.local == 'notfound') {
verseElement = chapterElement.findElements('v').firstWhere(
(element) => element.getAttribute('n') == verseNum.toString(),
orElse: () => XmlElement(XmlName('notfound')),
);
}
if (verseElement.name.local == 'notfound') {
return null;
}
// Extract the text content of the verse
return verseElement.innerText.trim();
}

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
@@ -27,12 +28,49 @@
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="icon" type="image/png" href="favicon.png" />
<title>christian_period_tracker</title>
<link rel="manifest" href="manifest.json">
<style>
body {
background-color: #FDFBFA;
/* Matching AppColors.cream */
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
}
.loader {
width: 50px;
height: 50px;
border: 5px solid #FDFBFA;
border-top: 5px solid #E5A4A4;
/* Matching AppColors.rose */
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<body id="app-container">
<div id="loading-indicator" class="loader"></div>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
</html>