From a799e9cf594d1ebf364afc8c26eb9700804db87f Mon Sep 17 00:00:00 2001 From: Sterlen Date: Fri, 9 Jan 2026 10:04:51 -0600 Subject: [PATCH] Resolve all lints and deprecation warnings --- .vscode/settings.json | 3 + devtools_options.yaml | 3 + lib/app_startup.dart | 153 ++ lib/data/learn_content.dart | 176 +- lib/main.dart | 42 +- lib/models/cycle_entry.dart | 2 +- lib/models/scripture.dart | 186 ++- lib/models/user_profile.dart | 12 +- lib/models/user_profile.g.dart | 12 +- lib/providers/scripture_provider.dart | 40 +- lib/screens/calendar/calendar_screen.dart | 62 +- lib/screens/devotional/devotional_screen.dart | 150 +- lib/screens/home/home_screen.dart | 103 +- .../husband/husband_appearance_screen.dart | 2 +- .../husband/husband_devotional_screen.dart | 39 +- lib/screens/husband/husband_home_screen.dart | 954 +---------- ...art => husband_learn_articles_screen.dart} | 171 +- lib/screens/husband/husband_notes_screen.dart | 18 +- .../husband/husband_settings_screen.dart | 26 +- lib/screens/husband/learn_article_screen.dart | 156 -- lib/screens/learn/husband_learn_screen.dart | 54 +- lib/screens/learn/wife_learn_screen.dart | 30 +- lib/screens/log/log_screen.dart | 1431 ++++++++--------- lib/screens/log/pad_tracker_screen.dart | 54 +- lib/screens/onboarding/onboarding_screen.dart | 61 +- lib/screens/settings/appearance_screen.dart | 68 +- .../settings/cycle_settings_screen.dart | 7 +- lib/screens/settings/export_data_screen.dart | 84 +- .../settings/goal_settings_screen.dart | 109 +- .../notification_settings_screen.dart | 39 +- .../settings/privacy_settings_screen.dart | 232 +-- .../relationship_settings_screen.dart | 154 +- .../settings/sharing_settings_screen.dart | 30 +- .../settings/supplies_settings_screen.dart | 204 ++- lib/screens/splash_screen.dart | 28 +- lib/services/bible_utils.dart | 22 +- lib/services/bible_xml_parser.dart | 70 +- lib/services/cycle_service.dart | 173 +- lib/services/health_service.dart | 13 +- lib/services/notification_service.dart | 11 +- lib/services/pdf_service.dart | 126 +- lib/theme/app_theme.dart | 34 +- lib/widgets/cycle_ring.dart | 21 +- lib/widgets/pad_settings_dialog.dart | 183 ++- lib/widgets/pad_tracker_card.dart | 11 +- lib/widgets/quick_log_buttons.dart | 15 +- lib/widgets/quick_log_dialog.dart | 8 +- lib/widgets/scripture_card.dart | 31 +- lib/widgets/tip_card.dart | 9 +- pubspec.lock | 6 +- pubspec.yaml | 3 + test/scripture_provider_test.dart | 53 +- test/scripture_test.dart | 19 +- test/widget_test.dart | 2 - tool/optimize_assets.dart | 271 ++-- web/index.html | 2 +- 56 files changed, 2819 insertions(+), 3159 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 devtools_options.yaml create mode 100644 lib/app_startup.dart rename lib/screens/husband/{_HusbandLearnScreen.dart => husband_learn_articles_screen.dart} (51%) delete mode 100644 lib/screens/husband/learn_article_screen.dart diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8c6c1ab --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "avdmanager.sdkPath": "/home/sterl/Downloads/Tracker-main/tracker/lib/providers" +} \ No newline at end of file diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/app_startup.dart b/lib/app_startup.dart new file mode 100644 index 0000000..d3ed8c3 --- /dev/null +++ b/lib/app_startup.dart @@ -0,0 +1,153 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'main.dart'; // Import ChristianPeriodTrackerApp +import 'models/cycle_entry.dart'; +import 'models/scripture.dart'; +import 'models/user_profile.dart'; +import 'screens/splash_screen.dart'; +import 'services/notification_service.dart'; + +class AppStartupWidget extends StatefulWidget { + const AppStartupWidget({super.key}); + + @override + State createState() => _AppStartupWidgetState(); +} + +class _AppStartupWidgetState extends State { + bool _isInitialized = false; + String _status = 'Initializing...'; + bool _showSkipButton = false; + final Duration _minSplashDuration = const Duration(milliseconds: 2000); + + @override + void initState() { + super.initState(); + _initializeApp(); + + // Show skip button if loading takes too long (e.g., 5 seconds) + Future.delayed(const Duration(seconds: 5), () { + if (mounted && !_isInitialized) { + setState(() => _showSkipButton = true); + } + }); + } + + Future _initializeApp() async { + final stopwatch = Stopwatch()..start(); + + try { + setState(() => _status = 'Loading user profile...'); + // Add timeout to prevent indefinite hanging + await Hive.openBox('user_profile') + .timeout(const Duration(seconds: 5)); + + setState(() => _status = 'Loading cycle data...'); + await Hive.openBox('cycle_entries') + .timeout(const Duration(seconds: 5)); + + setState(() => _status = 'Loading scriptures...'); + // Wrap scripture loading in a try-catch specifically to allow app to start even if assets fail + try { + await ScriptureDatabase() + .loadScriptures() + .timeout(const Duration(seconds: 5)); + } catch (e) { + debugPrint('Scripture loading failed (non-critical): $e'); + setState(() => _status = 'Scripture loading skipped...'); + } + + setState(() => _status = 'Setting up notifications...'); + try { + await NotificationService() + .initialize() + .timeout(const Duration(seconds: 3)); + } catch (e) { + debugPrint('Notification init failed: $e'); + } + } catch (e, stack) { + debugPrint('Initialization error: $e'); + debugPrint(stack.toString()); + setState(() => _status = 'Error: $e'); + // On critical error, allow user to proceed manually via skip button + setState(() => _showSkipButton = true); + return; + } + + final elapsed = stopwatch.elapsed; + if (elapsed < _minSplashDuration) { + await Future.delayed(_minSplashDuration - elapsed); + } + + if (mounted) { + setState(() { + _isInitialized = true; + }); + } + } + + void _skipInitialization() { + setState(() => _isInitialized = true); + } + + @override + Widget build(BuildContext context) { + if (!_isInitialized) { + return MaterialApp( + home: Scaffold( + body: Stack( + children: [ + const SplashScreen(isStartup: true), + + // Status Message (High Visibility) + Positioned( + bottom: 80, + left: 20, + right: 20, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + _status, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.black87, + fontSize: 14, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + fontFamily: 'SansSerif'), + ), + ), + ), + + // Skip Button + if (_showSkipButton) + Positioned( + bottom: 30, + left: 0, + right: 0, + child: Center( + child: ElevatedButton( + onPressed: _skipInitialization, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.redAccent, + foregroundColor: Colors.white, + ), + child: const Text('Skip Loading (Force Start)'), + ), + ), + ), + ], + ), + ), + debugShowCheckedModeBanner: false, + ); + } + + return const ChristianPeriodTrackerApp(); + } +} diff --git a/lib/data/learn_content.dart b/lib/data/learn_content.dart index 6f123fa..5fad513 100644 --- a/lib/data/learn_content.dart +++ b/lib/data/learn_content.dart @@ -1,3 +1,5 @@ +library learn_content; + /// Learn article content for the Husband section /// Contains educational articles about understanding her cycle, biblical manhood, and NFP @@ -28,7 +30,7 @@ class LearnSection { class LearnContent { static const List articles = [ // ========== UNDERSTANDING HER ========== - + LearnArticle( id: 'four_phases', title: 'The 4 Phases of Her Cycle', @@ -36,13 +38,15 @@ class LearnContent { category: 'Understanding Her', sections: [ LearnSection( - content: 'Your wife\'s body goes through four distinct phases each month, ' + 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 ' + 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, ' @@ -50,7 +54,8 @@ class LearnContent { ), LearnSection( heading: 'Dysmenorrhea (Painful Periods)', - content: 'While some cramping is normal, severe menstrual pain is called ' + 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 ' @@ -64,16 +69,17 @@ class LearnContent { ), LearnSection( heading: '2. Follicular Phase (Days 6-12)', - content: 'After her period ends, estrogen begins rising. This typically brings ' + 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. ' + 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 ' @@ -81,7 +87,8 @@ class LearnContent { ), LearnSection( heading: '4. Luteal Phase (Days 16-28)', - content: 'Progesterone rises after ovulation. If pregnancy doesn\'t occur, ' + 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, ' @@ -89,7 +96,8 @@ class LearnContent { ), LearnSection( heading: 'Remember', - content: '"Husbands, love your wives, just as Christ loved the church and gave ' + 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.', ), @@ -103,13 +111,15 @@ class LearnContent { category: 'Understanding Her', sections: [ LearnSection( - content: 'If you\'ve ever wondered why your wife seems like a different person ' + 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 ' + 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 ' @@ -120,7 +130,8 @@ class LearnContent { ), LearnSection( heading: 'It\'s Not "In Her Head"', - content: 'Studies show that hormonal fluctuations create real changes in brain chemistry. ' + 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.', @@ -135,7 +146,8 @@ class LearnContent { ), LearnSection( heading: 'Scripture to Remember', - content: '"Live with your wives in an understanding way, showing honor to the woman ' + 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', ), @@ -149,7 +161,8 @@ class LearnContent { category: 'Understanding Her', sections: [ LearnSection( - content: 'Premenstrual Syndrome (PMS) affects up to 90% of women to some degree. ' + 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.', ), @@ -176,7 +189,8 @@ class LearnContent { ), LearnSection( heading: 'PMDD: When It\'s Severe', - content: 'About 3-8% of women experience Premenstrual Dysphoric Disorder (PMDD), ' + 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.', @@ -193,7 +207,8 @@ class LearnContent { ), LearnSection( heading: 'A Husband\'s Prayer', - content: '"Lord, help me to be patient and understanding when my wife is struggling. ' + 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."', ), @@ -209,14 +224,16 @@ class LearnContent { category: 'Biblical Manhood', sections: [ LearnSection( - content: '"Husbands, love your wives, as Christ loved the church and gave himself ' + 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. ' + 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' @@ -225,7 +242,8 @@ class LearnContent { ), LearnSection( heading: 'Christ\'s Love is Patient', - content: 'Jesus was patient with His disciples\' failures and slow understanding. ' + 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' @@ -234,7 +252,8 @@ class LearnContent { ), LearnSection( heading: 'Christ\'s Love is Nourishing', - content: '"He nourishes and cherishes her" (Ephesians 5:29). For you, this means:\n\n' + 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' @@ -242,7 +261,8 @@ class LearnContent { ), LearnSection( heading: 'Christ\'s Love is Understanding', - content: 'Jesus knows our weaknesses because He experienced human flesh. ' + 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' @@ -251,7 +271,8 @@ class LearnContent { ), LearnSection( heading: 'Daily Application', - content: 'Each morning, ask yourself: "How can I love her like Christ today?"\n\n' + 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?"', ), ], @@ -264,13 +285,15 @@ class LearnContent { category: 'Biblical Manhood', sections: [ LearnSection( - content: '"For even the Son of Man came not to be served but to serve, and to give ' + 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' + 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' @@ -279,7 +302,8 @@ class LearnContent { ), LearnSection( heading: 'A Servant Leader Listens', - content: 'Great leaders listen more than they speak. When your wife is struggling:\n\n' + 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' @@ -295,7 +319,8 @@ class LearnContent { ), 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' + 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' @@ -319,7 +344,8 @@ class LearnContent { category: 'Biblical Manhood', sections: [ LearnSection( - content: 'Prayer is the most powerful thing you can do for your wife. It changes ' + 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( @@ -375,13 +401,15 @@ class LearnContent { category: 'NFP for Husbands', sections: [ LearnSection( - content: 'Natural Family Planning (NFP) isn\'t just your wife\'s responsibility—it\'s ' + 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 ' + 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' @@ -416,7 +444,8 @@ class LearnContent { ), LearnSection( heading: 'Scripture', - content: '"Two are better than one, because they have a good reward for their toil. ' + 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', ), ], @@ -429,13 +458,15 @@ class LearnContent { category: 'NFP for Husbands', sections: [ LearnSection( - content: 'For couples practicing NFP to avoid pregnancy, abstaining during the ' + 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' + 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' @@ -443,7 +474,8 @@ class LearnContent { ), LearnSection( heading: 'Reframing Abstinence', - content: 'Instead of seeing fertile days as deprivation, see them as:\n\n' + 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' @@ -452,7 +484,8 @@ class LearnContent { ), LearnSection( heading: 'Ways to Stay Connected', - content: 'During fertile days, intentionally build closeness through:\n\n' + 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' @@ -471,7 +504,8 @@ class LearnContent { ), LearnSection( heading: 'A Prayer for Difficult Days', - content: '"Lord, this is hard. Help me to love her well even when I\'m frustrated. ' + 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."', ), @@ -486,34 +520,39 @@ class LearnContent { category: 'Understanding My Cycle', sections: [ LearnSection( - content: 'Your menstrual cycle is more than just your period. It\'s a continuous ' + content: + 'Your menstrual cycle is more than just your period. It\'s a continuous ' 'rhythm of hormones that affects your energy, mood, and body. Understanding ' 'these four phases empowers you to work with your body, rather than against it.', ), LearnSection( heading: '1. Menstrual Phase (Days 1-5)', - content: 'The start of your cycle. Progesterone plunges, causing the uterine lining ' + content: + 'The start of your cycle. Progesterone plunges, causing the uterine lining ' 'to shed. Energy typically dips.\n\n' '💡 Tips: Prioritize rest. Warm foods and gentle movement (like walking or stretching) ' 'can help with cramping. Don\'t push yourself to be highly productive if you don\'t feel up to it.', ), LearnSection( heading: '2. Follicular Phase (Days 6-12)', - content: 'Estrogen rises as your body prepares an egg. You likely feel a boost in energy, ' + content: + 'Estrogen rises as your body prepares an egg. You likely feel a boost in energy, ' 'creativity, and social desire. Your skin may clear up and you feel more optimistic.\n\n' '💡 Tips: This is a great time to start new projects, exercise harder, and schedule ' 'social gatherings.', ), LearnSection( heading: '3. Ovulation Phase (Days 13-15)', - content: 'Peak fertility. Estrogen is high, and you may feel your most confident and vibrant. ' + content: + 'Peak fertility. Estrogen is high, and you may feel your most confident and vibrant. ' 'Libido often increases. You are magnetic!\n\n' '💡 Tips: Schedule date nights or important conversations. You are likely more ' 'articulate and persuasive now.', ), LearnSection( heading: '4. Luteal Phase (Days 16-28)', - content: 'Progesterone acts as a "braking" hormone. Energy winds down. You may turn inward ' + content: + 'Progesterone acts as a "braking" hormone. Energy winds down. You may turn inward ' 'and feel more reflective. PMS symptoms may appear near the end.\n\n' '💡 Tips: Be gentle with yourself. Focus on completing tasks rather than starting new ones. ' 'Organize your space and prepare for the rest phase coming next.', @@ -528,23 +567,27 @@ class LearnContent { category: 'Understanding My Cycle', sections: [ LearnSection( - content: 'It is not "all in your head." Hormones like estrogen and progesterone ' + content: + 'It is not "all in your head." Hormones like estrogen and progesterone ' 'directly impact brain chemistry and neurotransmitters like serotonin. ' 'Fluctuating emotions are a biological reality.', ), LearnSection( heading: 'Estrogen: The Uplifter', - content: 'When estrogen is high (Follicular/Ovulation), it boosts serotonin and dopamine. ' + content: + 'When estrogen is high (Follicular/Ovulation), it boosts serotonin and dopamine. ' 'You feel resilient, happy, and outgoing.', ), LearnSection( heading: 'Progesterone: The Sedative', - content: 'Rising in the Luteal phase, progesterone has a calming effect but can also ' + content: + 'Rising in the Luteal phase, progesterone has a calming effect but can also ' 'lead to feelings of sadness or sluggishness if levels aren\'t balanced.', ), LearnSection( heading: 'The Drop', - content: 'Just before your period, both hormones drop. This withdrawal can cause ' + content: + 'Just before your period, both hormones drop. This withdrawal can cause ' 'irritability, anxiety, and tearfulness (PMS). It is temporary!\n\n' 'Scripture: "I praise you, for I am fearfully and wonderfully made." — Psalm 139:14', ), @@ -558,12 +601,14 @@ class LearnContent { category: 'Disease Prevention', sections: [ LearnSection( - content: 'Maintaining reproductive health involves regular hygiene and awareness ' + content: + 'Maintaining reproductive health involves regular hygiene and awareness ' 'of your body\'s natural defenses.', ), LearnSection( heading: 'Hygiene Basics', - content: '• **Wiping**: Always wipe from front to back to prevent bacteria from ' + content: + '• **Wiping**: Always wipe from front to back to prevent bacteria from ' 'entering the urethra or vagina.\n' '• **Cleaning**: Use warm water. Avoid harsh soaps, douches, or scented products ' 'internally, as they disrupt the natural pH balance and can lead to yeast infections or BV.\n' @@ -571,14 +616,16 @@ class LearnContent { ), LearnSection( heading: 'UTI Prevention', - content: 'Urinary Tract Infections (UTIs) are common but preventable:\n' + content: + 'Urinary Tract Infections (UTIs) are common but preventable:\n' '• Hydrate well to flush out bacteria.\n' '• Urinate after intercourse to clear the urethra.\n' '• Don\'t hold urine for long periods.', ), LearnSection( heading: 'STI Prevention', - content: 'Sexually Transmitted Infections can affect anyone. If you or your partner ' + content: + 'Sexually Transmitted Infections can affect anyone. If you or your partner ' 'have a history of STIs, open communication and testing are crucial.\n\n' 'Monogamy within marriage provides the safest environment for sexual health.', ), @@ -592,23 +639,27 @@ class LearnContent { category: 'Disease Prevention', sections: [ LearnSection( - content: 'Regular medical check-ups are vital for catching issues early when they ' + content: + 'Regular medical check-ups are vital for catching issues early when they ' 'are most treatable.', ), LearnSection( heading: 'Pap Smears & HPV Testing', - content: '• **What**: Checks for cervical cancer and HPV (the virus that causes it).\n' + content: + '• **What**: Checks for cervical cancer and HPV (the virus that causes it).\n' '• **When**: Typically every 3-5 years for women aged 21-65, depending on your doctor\'s advice.', ), LearnSection( heading: 'Breast Self-Exams', - content: '• **What**: Feeling for lumps or changes in breast tissue.\n' + content: + '• **What**: Feeling for lumps or changes in breast tissue.\n' '• **When**: Once a month, ideally a few days after your period ends when ' 'breasts are least tender.', ), LearnSection( heading: 'Annual Well-Woman Exam', - content: 'Even if you don\'t need a Pap smear locally, an annual visit allows you to discuss ' + content: + 'Even if you don\'t need a Pap smear locally, an annual visit allows you to discuss ' 'period pain, fertility, contraception, and overall wellness with your provider.', ), ], @@ -621,7 +672,8 @@ class LearnContent { category: 'Partnership', sections: [ LearnSection( - content: 'Your husband likely wants to support you but may not know how. ' + content: + 'Your husband likely wants to support you but may not know how. ' 'Clear communication bridges that gap.', ), LearnSection( @@ -632,31 +684,35 @@ class LearnContent { ), LearnSection( heading: 'Share Your Cycle', - content: ' Invite him into the process. Let him know when your period starts ' + content: + ' Invite him into the process. Let him know when your period starts ' 'or when you are ovulating. This helps him understand your changing needs ' 'and moods.', ), ], ), - LearnArticle( + LearnArticle( id: 'wife_shared_responsibility', title: 'Shared Responsibility', subtitle: 'Navigating fertility together', category: 'Partnership', sections: [ LearnSection( - content: 'Fertility is a shared reality, not just "the woman\'s job." ' + content: + 'Fertility is a shared reality, not just "the woman\'s job." ' 'Whether trying to conceive or avoiding pregnancy, walk this path together.', ), LearnSection( - heading: 'NFP / FAM', - content: 'If practicing Natural Family Planning, involve him in chart reading. ' - 'Discuss the fertile window together. It builds intimacy and trust.', + heading: 'NFP / FAM', + content: + 'If practicing Natural Family Planning, involve him in chart reading. ' + 'Discuss the fertile window together. It builds intimacy and trust.', ), LearnSection( heading: 'Mutual Care', - content: 'Scripture calls husbands to love their wives as their own bodies. ' + content: + 'Scripture calls husbands to love their wives as their own bodies. ' 'Allow him to care for you, and seek to understand his perspective as well.', ), ], diff --git a/lib/main.dart b/lib/main.dart index b32e1ba..c8b0d1b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,13 +2,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hive_flutter/hive_flutter.dart'; -import 'models/scripture.dart'; import 'theme/app_theme.dart'; -import 'screens/splash_screen.dart'; + +import 'screens/onboarding/onboarding_screen.dart'; +import 'screens/home/home_screen.dart'; +import 'screens/husband/husband_home_screen.dart'; import 'models/user_profile.dart'; import 'models/cycle_entry.dart'; +import 'models/teaching_plan.dart'; +import 'models/scripture.dart'; import 'providers/user_provider.dart'; -import 'services/notification_service.dart'; +import 'app_startup.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -28,24 +32,14 @@ void main() async { Hive.registerAdapter(UserRoleAdapter()); Hive.registerAdapter(BibleTranslationAdapter()); Hive.registerAdapter(ScriptureAdapter()); - Hive.registerAdapter(AppThemeModeAdapter()); // Register new adapter + Hive.registerAdapter(AppThemeModeAdapter()); Hive.registerAdapter(SupplyItemAdapter()); Hive.registerAdapter(PadTypeAdapter()); + Hive.registerAdapter(TeachingPlanAdapter()); - // Open boxes and load scriptures in parallel - await Future.wait([ - Hive.openBox('user_profile'), - Hive.openBox('cycle_entries'), - ScriptureDatabase().loadScriptures(), - ]); - - // Initialize notifications - await NotificationService().initialize(); - - runApp(const ProviderScope(child: ChristianPeriodTrackerApp())); + runApp(const ProviderScope(child: AppStartupWidget())); } - // Helper to convert hex string to Color Color _colorFromHex(String hexColor) { try { @@ -61,6 +55,7 @@ class ChristianPeriodTrackerApp extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + // Watch user profile to determine final userProfile = ref.watch(userProfileProvider); final ThemeMode themeMode; @@ -74,9 +69,8 @@ class ChristianPeriodTrackerApp extends ConsumerWidget { break; case AppThemeMode.dark: themeMode = ThemeMode.dark; - break; + case AppThemeMode.system: - default: themeMode = ThemeMode.system; break; } @@ -86,13 +80,23 @@ class ChristianPeriodTrackerApp extends ConsumerWidget { accentColor = AppColors.sageGreen; } + // Determine the home screen based on profile state + Widget homeScreen; + if (userProfile == null) { + homeScreen = const OnboardingScreen(); + } else if (userProfile.role == UserRole.husband) { + homeScreen = const HusbandHomeScreen(); + } else { + homeScreen = const HomeScreen(); + } + return MaterialApp( title: 'Christian Period Tracker', debugShowCheckedModeBanner: false, theme: AppTheme.getLightTheme(accentColor), darkTheme: AppTheme.getDarkTheme(accentColor), themeMode: themeMode, - home: const SplashScreen(), + home: homeScreen, ); } } diff --git a/lib/models/cycle_entry.dart b/lib/models/cycle_entry.dart index c3870e6..6d01da6 100644 --- a/lib/models/cycle_entry.dart +++ b/lib/models/cycle_entry.dart @@ -418,7 +418,7 @@ extension CyclePhaseExtension on CyclePhase { return [ AppColors.sageGreen, AppColors.follicularPhase, - AppColors.sageGreen.withOpacity(0.7) + AppColors.sageGreen.withValues(alpha: 0.7) ]; case CyclePhase.ovulation: return [AppColors.lavender, AppColors.ovulationPhase, AppColors.rose]; diff --git a/lib/models/scripture.dart b/lib/models/scripture.dart index d599c91..9b24451 100644 --- a/lib/models/scripture.dart +++ b/lib/models/scripture.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:math'; import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart'; // For debugPrint import 'package:hive_flutter/hive_flutter.dart'; // Import Hive import '../services/bible_xml_parser.dart'; // Import the XML parser @@ -39,12 +40,12 @@ class Scripture extends HiveObject { reference: json['reference'], reflection: json['reflection'], applicablePhases: (json['applicablePhases'] as List?) - ?.map((e) => e as String) - .toList() ?? + ?.map((e) => e as String) + .toList() ?? [], applicableContexts: (json['applicableContexts'] as List?) - ?.map((e) => e as String) - .toList() ?? + ?.map((e) => e as String) + .toList() ?? [], ); } @@ -54,6 +55,7 @@ class Scripture extends HiveObject { verses[BibleTranslation.esv] ?? verses.values.first; } + @override bool operator ==(Object other) => identical(this, other) || @@ -92,7 +94,9 @@ class Scripture extends HiveObject { 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 + if (!b.containsKey(key) || a[key] != b[key]) { + return false; // Added containsKey check + } } return true; } @@ -103,26 +107,14 @@ class ScriptureDatabase { static final ScriptureDatabase _instance = ScriptureDatabase._internal(); factory ScriptureDatabase({BibleXmlParser? bibleXmlParser}) { - _instance._bibleXmlParser = bibleXmlParser ?? BibleXmlParser(); return _instance; } ScriptureDatabase._internal(); - late BibleXmlParser _bibleXmlParser; - late Box _scriptureBox; // Mapping of BibleTranslation to its XML asset path - final Map _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 _menstrualScriptures = []; List _follicularScriptures = []; @@ -137,17 +129,21 @@ class ScriptureDatabase { 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.", + 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?", + 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.", + 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'], @@ -156,8 +152,10 @@ class ScriptureDatabase { 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.", + 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'], @@ -166,26 +164,32 @@ class ScriptureDatabase { 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.", + 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.", + 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?", + 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.", + 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.", + 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'], @@ -206,9 +210,11 @@ class ScriptureDatabase { _scriptureBox = await Hive.openBox('scriptures'); if (_scriptureBox.isEmpty) { - print('Hive box is empty. Importing scriptures from optimized JSON data...'); + debugPrint( + '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 String response = + await rootBundle.loadString('assets/scriptures_optimized.json'); final Map data = json.decode(response); List importedScriptures = []; @@ -218,28 +224,32 @@ class ScriptureDatabase { for (var jsonEntry in list) { final reference = jsonEntry['reference']; final reflection = jsonEntry['reflection']; // Optional - - final applicablePhases = (jsonEntry['applicablePhases'] as List?) - ?.map((e) => e as String) - .toList() ?? []; - - final applicableContexts = (jsonEntry['applicableContexts'] as List?) - ?.map((e) => e as String) - .toList() ?? []; + + final applicablePhases = + (jsonEntry['applicablePhases'] as List?) + ?.map((e) => e as String) + .toList() ?? + []; + + final applicableContexts = + (jsonEntry['applicableContexts'] as List?) + ?.map((e) => e as String) + .toList() ?? + []; // Map string keys (esv, niv) to BibleTranslation enum Map versesMap = {}; if (jsonEntry['verses'] != null) { (jsonEntry['verses'] as Map).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'); - } + // 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) { + debugPrint( + 'Warning: Unknown translation key "$key" in optimized JSON'); + } }); } @@ -255,15 +265,26 @@ class ScriptureDatabase { } } - // 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['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; contextualMap.forEach((key, value) { @@ -272,11 +293,14 @@ class ScriptureDatabase { } // Store all imported scriptures into Hive - for (var scripture in importedScriptures) { - await _scriptureBox.put(scripture.reference, scripture); // Using reference as key - } + final Map scripturesMap = { + for (var s in importedScriptures) s.reference: s + }; + await _scriptureBox.putAll(scripturesMap); + debugPrint( + 'Successfully imported ${importedScriptures.length} scriptures.'); } else { - print('Hive box is not empty. Loading scriptures from Hive...'); + debugPrint('Hive box is not empty. Loading scriptures from Hive...'); } // Populate internal lists from Hive box values @@ -293,7 +317,8 @@ class ScriptureDatabase { .where((s) => s.applicablePhases.contains('luteal')) .toList(); _husbandScriptures = [ - ..._scriptureBox.values.where((s) => s.applicablePhases.contains('husband')), + ..._scriptureBox.values + .where((s) => s.applicablePhases.contains('husband')), ..._hardcodedHusbandScriptures, ]; // Remove duplicates based on reference if any @@ -308,10 +333,18 @@ class ScriptureDatabase { .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(), + '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(), }; } @@ -421,7 +454,6 @@ class ScriptureDatabase { return scriptures[index]; } - // ... imports // ... inside ScriptureDatabase class @@ -518,10 +550,22 @@ class ScriptureDatabase { ...(_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: []); + } } - 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)]; } @@ -529,7 +573,11 @@ class ScriptureDatabase { Scripture getHusbandScripture() { final scriptures = _husbandScriptures; if (scriptures.isEmpty) { - return Scripture(verses: {BibleTranslation.esv: "No husband scripture found."}, reference: "Unknown", applicablePhases: [], applicableContexts: []); + return Scripture( + verses: {BibleTranslation.esv: "No husband scripture found."}, + reference: "Unknown", + applicablePhases: [], + applicableContexts: []); } return scriptures[Random().nextInt(scriptures.length)]; } diff --git a/lib/models/user_profile.dart b/lib/models/user_profile.dart index 2e734a1..ac8af98 100644 --- a/lib/models/user_profile.dart +++ b/lib/models/user_profile.dart @@ -64,13 +64,13 @@ enum PadType { @HiveField(1) regular, @HiveField(2) - super_pad, + superPad, @HiveField(3) overnight, @HiveField(4) - tampon_regular, + tamponRegular, @HiveField(5) - tampon_super, + tamponSuper, @HiveField(6) menstrualCup, @HiveField(7) @@ -86,13 +86,13 @@ extension PadTypeExtension on PadType { return 'Liner'; case PadType.regular: return 'Regular Pad'; - case PadType.super_pad: + case PadType.superPad: return 'Super Pad'; case PadType.overnight: return 'Overnight'; - case PadType.tampon_regular: + case PadType.tamponRegular: return 'Tampon (Regular)'; - case PadType.tampon_super: + case PadType.tamponSuper: return 'Tampon (Super)'; case PadType.menstrualCup: return 'Cup'; diff --git a/lib/models/user_profile.g.dart b/lib/models/user_profile.g.dart index 3d09bc3..2a4298a 100644 --- a/lib/models/user_profile.g.dart +++ b/lib/models/user_profile.g.dart @@ -463,13 +463,13 @@ class PadTypeAdapter extends TypeAdapter { case 1: return PadType.regular; case 2: - return PadType.super_pad; + return PadType.superPad; case 3: return PadType.overnight; case 4: - return PadType.tampon_regular; + return PadType.tamponRegular; case 5: - return PadType.tampon_super; + return PadType.tamponSuper; case 6: return PadType.menstrualCup; case 7: @@ -490,16 +490,16 @@ class PadTypeAdapter extends TypeAdapter { case PadType.regular: writer.writeByte(1); break; - case PadType.super_pad: + case PadType.superPad: writer.writeByte(2); break; case PadType.overnight: writer.writeByte(3); break; - case PadType.tampon_regular: + case PadType.tamponRegular: writer.writeByte(4); break; - case PadType.tampon_super: + case PadType.tamponSuper: writer.writeByte(5); break; case PadType.menstrualCup: diff --git a/lib/providers/scripture_provider.dart b/lib/providers/scripture_provider.dart index 675eae3..3682ef5 100644 --- a/lib/providers/scripture_provider.dart +++ b/lib/providers/scripture_provider.dart @@ -1,8 +1,7 @@ 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 +// For IterableExtension // State for ScriptureProvider class ScriptureState { @@ -36,9 +35,8 @@ class ScriptureState { // StateNotifier for ScriptureProvider class ScriptureNotifier extends StateNotifier { final ScriptureDatabase _scriptureDatabase; - final Ref _ref; - ScriptureNotifier(this._scriptureDatabase, this._ref) : super(ScriptureState()) { + ScriptureNotifier(this._scriptureDatabase) : super(ScriptureState()) { // We don't initialize here directly, as we need the phase from other providers. // Initialization will be triggered by the UI. } @@ -48,11 +46,13 @@ class ScriptureNotifier extends StateNotifier { Future 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); + 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 dayOfYear = DateTime.now() + .difference(DateTime(DateTime.now().year, 1, 1)) + .inDays; final initialIndex = dayOfYear % scriptureCount; state = state.copyWith( currentPhase: phase, @@ -73,24 +73,38 @@ class ScriptureNotifier extends StateNotifier { } void getNextScripture() { - if (state.currentPhase == null || state.maxIndex == null || state.maxIndex == 0) return; + 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; + if (state.currentPhase == null || + state.maxIndex == null || + state.maxIndex == 0) { + return; + } - final prevIndex = (state.currentIndex - 1 + state.maxIndex!) % state.maxIndex!; + final prevIndex = + (state.currentIndex - 1 + state.maxIndex!) % state.maxIndex!; _updateScripture(prevIndex); } void getRandomScripture() { - if (state.currentPhase == null || state.maxIndex == null || state.maxIndex == 0) return; + 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 + final randomIndex = DateTime.now().microsecondsSinceEpoch % + state.maxIndex!; // Still using timestamp for simplicity _updateScripture(randomIndex); } @@ -109,5 +123,5 @@ final scriptureDatabaseProvider = Provider((ref) => ScriptureDatabase()); final scriptureProvider = StateNotifierProvider((ref) { - return ScriptureNotifier(ref.watch(scriptureDatabaseProvider), ref); + return ScriptureNotifier(ref.watch(scriptureDatabaseProvider)); }); diff --git a/lib/screens/calendar/calendar_screen.dart b/lib/screens/calendar/calendar_screen.dart index 89fd0ad..f239e36 100644 --- a/lib/screens/calendar/calendar_screen.dart +++ b/lib/screens/calendar/calendar_screen.dart @@ -6,7 +6,6 @@ import 'package:table_calendar/table_calendar.dart'; import '../../models/user_profile.dart'; import '../../models/cycle_entry.dart'; import '../../providers/user_provider.dart'; -import '../../services/cycle_service.dart'; import '../../theme/app_theme.dart'; import '../log/log_screen.dart'; import 'package:uuid/uuid.dart'; @@ -96,7 +95,7 @@ class _CalendarScreenState extends ConsumerState { borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 15, offset: const Offset(0, 5), ), @@ -134,7 +133,7 @@ class _CalendarScreenState extends ConsumerState { AppColors.charcoal, ), todayDecoration: BoxDecoration( - color: AppColors.sageGreen.withOpacity(0.3), + color: AppColors.sageGreen.withValues(alpha: 0.3), shape: BoxShape.circle, ), todayTextStyle: GoogleFonts.outfit( @@ -303,7 +302,7 @@ class _CalendarScreenState extends ConsumerState { return Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( - color: AppColors.lightGray.withOpacity(0.5), + color: AppColors.lightGray.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(12), ), child: Row( @@ -332,7 +331,7 @@ class _CalendarScreenState extends ConsumerState { boxShadow: isSelected ? [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 4, offset: const Offset(0, 2), ) @@ -359,12 +358,12 @@ class _CalendarScreenState extends ConsumerState { child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: AppColors.blushPink.withOpacity(0.5), + color: AppColors.blushPink.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(20), ), child: Row( children: [ - Icon(Icons.info_outline, size: 16, color: AppColors.rose), + const Icon(Icons.info_outline, size: 16, color: AppColors.rose), const SizedBox(width: 4), Text( 'Legend', @@ -451,7 +450,7 @@ class _CalendarScreenState extends ConsumerState { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -482,7 +481,7 @@ class _CalendarScreenState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: _getPhaseColor(phase).withOpacity(0.15), + color: _getPhaseColor(phase).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(20), ), child: Row( @@ -625,9 +624,10 @@ class _CalendarScreenState extends ConsumerState { margin: const EdgeInsets.only(top: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: AppColors.menstrualPhase.withOpacity(0.05), + color: AppColors.menstrualPhase.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.menstrualPhase.withOpacity(0.2)), + border: + Border.all(color: AppColors.menstrualPhase.withValues(alpha: 0.2)), ), child: Row( children: [ @@ -703,9 +703,9 @@ class _CalendarScreenState extends ConsumerState { margin: const EdgeInsets.only(top: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: AppColors.softGold.withOpacity(isDark ? 0.15 : 0.1), + color: AppColors.softGold.withValues(alpha: isDark ? 0.15 : 0.1), borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.softGold.withOpacity(0.3)), + border: Border.all(color: AppColors.softGold.withValues(alpha: 0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -758,7 +758,7 @@ class _CalendarScreenState extends ConsumerState { Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: color.withOpacity(0.1), + color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: color, size: 18), @@ -791,13 +791,24 @@ class _CalendarScreenState extends ConsumerState { String _getSymptomsString(CycleEntry entry) { List s = []; - if (entry.crampIntensity != null && entry.crampIntensity! > 0) + 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'); - if (entry.hasFatigue) s.add('Fatigue'); - if (entry.hasAcne) s.add('Acne'); + } + if (entry.hasHeadache) { + s.add('Headache'); + } + if (entry.hasBloating) { + s.add('Bloating'); + } + if (entry.hasBreastTenderness) { + s.add('Breast Tenderness'); + } + if (entry.hasFatigue) { + s.add('Fatigue'); + } + if (entry.hasAcne) { + s.add('Acne'); + } return s.join(', '); } @@ -856,14 +867,9 @@ class _CalendarScreenState extends ConsumerState { return months[month - 1]; } - bool _isLoggedPeriodDay(DateTime date, List entries) { - final entry = _getEntryForDate(date, entries); - return entry?.isPeriodDay ?? false; - } - Widget _buildCalendarDay(DateTime day, DateTime focusedDay, List entries, DateTime? lastPeriodStart, int cycleLength, - {bool isSelected = false, bool isToday = false, bool isWeekend = false}) { + {bool isSelected = false, bool isToday = false}) { final phase = _getPhaseForDate(day, lastPeriodStart, cycleLength); final isDark = Theme.of(context).brightness == Brightness.dark; @@ -888,12 +894,12 @@ class _CalendarScreenState extends ConsumerState { ); } else if (isToday) { decoration = BoxDecoration( - color: AppColors.sageGreen.withOpacity(0.2), + color: AppColors.sageGreen.withValues(alpha: 0.2), shape: BoxShape.circle, ); } else if (phase != null) { decoration = BoxDecoration( - color: _getPhaseColor(phase).withOpacity(isDark ? 0.2 : 0.15), + color: _getPhaseColor(phase).withValues(alpha: isDark ? 0.2 : 0.15), shape: BoxShape.circle, ); } diff --git a/lib/screens/devotional/devotional_screen.dart b/lib/screens/devotional/devotional_screen.dart index 8a509dd..eb4406d 100644 --- a/lib/screens/devotional/devotional_screen.dart +++ b/lib/screens/devotional/devotional_screen.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import '../../models/scripture.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../providers/user_provider.dart'; import '../../services/cycle_service.dart'; @@ -54,7 +53,7 @@ class _DevotionalScreenState extends ConsumerState { ...BibleTranslation.values.map((t) => ListTile( title: Text(t.label), trailing: user.bibleTranslation == t - ? Icon(Icons.check, color: AppColors.sageGreen) + ? const Icon(Icons.check, color: AppColors.sageGreen) : null, onTap: () => Navigator.pop(context, t), )), @@ -73,7 +72,8 @@ class _DevotionalScreenState extends ConsumerState { @override Widget build(BuildContext context) { // Listen for changes in the cycle info to re-initialize scripture if needed - ref.listen(currentCycleInfoProvider, (previousCycleInfo, newCycleInfo) { + ref.listen(currentCycleInfoProvider, + (previousCycleInfo, newCycleInfo) { if (previousCycleInfo?.phase != newCycleInfo.phase) { _initializeScripture(); } @@ -91,7 +91,8 @@ class _DevotionalScreenState extends ConsumerState { final maxIndex = scriptureState.maxIndex; if (scripture == null) { - return const Center(child: CircularProgressIndicator()); // Or some error message + return const Center( + child: CircularProgressIndicator()); // Or some error message } return SafeArea( @@ -117,7 +118,7 @@ class _DevotionalScreenState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( - color: _getPhaseColor(phase).withOpacity(0.15), + color: _getPhaseColor(phase).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(20), ), child: Row( @@ -165,18 +166,20 @@ class _DevotionalScreenState extends ConsumerState { Positioned( left: 0, child: IconButton( - icon: Icon(Icons.arrow_back_ios), - onPressed: () => - ref.read(scriptureProvider.notifier).getPreviousScripture(), + icon: const 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(), + icon: const Icon(Icons.arrow_forward_ios), + onPressed: () => ref + .read(scriptureProvider.notifier) + .getNextScripture(), color: AppColors.charcoal, ), ), @@ -185,16 +188,17 @@ class _DevotionalScreenState extends ConsumerState { ), 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, + Center( + child: TextButton.icon( + onPressed: () => + ref.read(scriptureProvider.notifier).getRandomScripture(), + icon: const Icon(Icons.shuffle), + label: const Text('Random Verse'), + style: TextButton.styleFrom( + foregroundColor: AppColors.sageGreen, + ), ), ), - ), const SizedBox(height: 24), // Reflection @@ -207,7 +211,7 @@ class _DevotionalScreenState extends ConsumerState { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -216,20 +220,20 @@ class _DevotionalScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + const Row( children: [ Icon( Icons.lightbulb_outline, color: AppColors.softGold, size: 20, ), - const SizedBox(width: 8), + SizedBox(width: 8), Text( 'Reflection', - style: GoogleFonts.outfit( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Theme.of(context).textTheme.titleLarge?.color, + color: AppColors.charcoal, ), ), ], @@ -258,7 +262,7 @@ class _DevotionalScreenState extends ConsumerState { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -269,7 +273,7 @@ class _DevotionalScreenState extends ConsumerState { children: [ Row( children: [ - Icon( + const Icon( Icons.favorite_outline, color: AppColors.rose, size: 20, @@ -306,8 +310,8 @@ class _DevotionalScreenState extends ConsumerState { decoration: BoxDecoration( gradient: LinearGradient( colors: [ - AppColors.lavender.withOpacity(isDark ? 0.35 : 0.2), - AppColors.blushPink.withOpacity(isDark ? 0.35 : 0.2), + AppColors.lavender.withValues(alpha: isDark ? 0.35 : 0.2), + AppColors.blushPink.withValues(alpha: isDark ? 0.35 : 0.2), ], begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -317,16 +321,16 @@ class _DevotionalScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + const Row( children: [ Text('🙏', style: TextStyle(fontSize: 20)), - const SizedBox(width: 8), + SizedBox(width: 8), Text( 'Prayer Prompt', - style: GoogleFonts.outfit( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Theme.of(context).textTheme.titleLarge?.color, + color: AppColors.charcoal, // Assuming a default color ), ), ], @@ -351,8 +355,8 @@ class _DevotionalScreenState extends ConsumerState { if (user.teachingPlans?.isNotEmpty ?? false) _buildTeachingPlanCard(context, user.teachingPlans!) else - _buildSampleTeachingCard(context), - + _buildSampleTeachingCard(context), + const SizedBox(height: 24), // Action buttons @@ -434,32 +438,34 @@ class _DevotionalScreenState extends ConsumerState { 'Help me to serve with joy and purpose. Amen."'; case CyclePhase.ovulation: return '"Creator God, I am fearfully and wonderfully made. ' - 'Thank You for the gift of womanhood. ' - 'Help me to honor You in all I do today. Amen."'; + 'Thank You for the gift of womanhood. ' + 'Help me to honor You in all I do today. Amen."'; case CyclePhase.luteal: return '"Lord, I bring my anxious thoughts to You. ' - 'When my emotions feel overwhelming, remind me of Your peace. ' - 'Help me to be gentle with myself as You are gentle with me. Amen."'; + 'When my emotions feel overwhelming, remind me of Your peace. ' + 'Help me to be gentle with myself as You are gentle with me. Amen."'; } } - Widget _buildTeachingPlanCard(BuildContext context, List plans) { + Widget _buildTeachingPlanCard( + BuildContext context, List plans) { // Get latest uncompleted plan or just latest if (plans.isEmpty) return const SizedBox.shrink(); // Sort by date desc - final sorted = List.from(plans)..sort((a,b) => b.date.compareTo(a.date)); + final sorted = List.from(plans) + ..sort((a, b) => b.date.compareTo(a.date)); final latestPlan = sorted.first; - + return Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.gold.withOpacity(0.5)), + border: Border.all(color: AppColors.gold.withValues(alpha: 0.5)), boxShadow: [ BoxShadow( - color: AppColors.gold.withOpacity(0.1), + color: AppColors.gold.withValues(alpha: 0.1), blurRadius: 10, offset: const Offset(0, 4), ), @@ -470,13 +476,13 @@ class _DevotionalScreenState extends ConsumerState { children: [ Row( children: [ - Icon(Icons.menu_book, color: AppColors.navyBlue), + const Icon(Icons.menu_book, color: AppColors.navyBlue), const SizedBox(width: 8), Expanded( child: Text( 'Leading in the Word', style: GoogleFonts.outfit( - fontSize: 16, + fontSize: 16, fontWeight: FontWeight.bold, color: AppColors.navyBlue, ), @@ -485,12 +491,15 @@ class _DevotionalScreenState extends ConsumerState { Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: AppColors.gold.withOpacity(0.1), + color: AppColors.gold.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Text( 'Husband\'s Sharing', - style: GoogleFonts.outfit(fontSize: 10, color: AppColors.gold, fontWeight: FontWeight.bold), + style: GoogleFonts.outfit( + fontSize: 10, + color: AppColors.gold, + fontWeight: FontWeight.bold), ), ), ], @@ -521,7 +530,7 @@ class _DevotionalScreenState extends ConsumerState { style: GoogleFonts.lora( fontSize: 15, height: 1.5, - color: AppColors.charcoal.withOpacity(0.9), + color: AppColors.charcoal.withValues(alpha: 0.9), ), ), ], @@ -536,10 +545,12 @@ class _DevotionalScreenState extends ConsumerState { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.warmGray.withOpacity(0.3), style: BorderStyle.solid), + border: Border.all( + color: AppColors.warmGray.withValues(alpha: 0.3), + style: BorderStyle.solid), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -550,13 +561,13 @@ class _DevotionalScreenState extends ConsumerState { children: [ Row( children: [ - Icon(Icons.menu_book, color: AppColors.warmGray), + const Icon(Icons.menu_book, color: AppColors.warmGray), const SizedBox(width: 12), Expanded( child: Text( 'Leading in the Word', style: GoogleFonts.outfit( - fontSize: 16, + fontSize: 16, fontWeight: FontWeight.bold, color: AppColors.warmGray, ), @@ -565,12 +576,15 @@ class _DevotionalScreenState extends ConsumerState { Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: AppColors.warmGray.withOpacity(0.1), + color: AppColors.warmGray.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Text( 'Sample', - style: GoogleFonts.outfit(fontSize: 10, color: AppColors.warmGray, fontWeight: FontWeight.bold), + style: GoogleFonts.outfit( + fontSize: 10, + color: AppColors.warmGray, + fontWeight: FontWeight.bold), ), ), ], @@ -581,7 +595,7 @@ class _DevotionalScreenState extends ConsumerState { style: GoogleFonts.outfit( fontSize: 18, fontWeight: FontWeight.w600, - color: AppColors.charcoal.withOpacity(0.7), + color: AppColors.charcoal.withValues(alpha: 0.7), ), ), const SizedBox(height: 4), @@ -600,11 +614,11 @@ class _DevotionalScreenState extends ConsumerState { fontSize: 15, height: 1.5, fontStyle: FontStyle.italic, - color: AppColors.charcoal.withOpacity(0.6), + color: AppColors.charcoal.withValues(alpha: 0.6), ), ), const SizedBox(height: 16), - Center( + Center( child: OutlinedButton.icon( onPressed: () => _showShareDialog(context), icon: const Icon(Icons.link, size: 18), @@ -614,7 +628,7 @@ class _DevotionalScreenState extends ConsumerState { side: const BorderSide(color: AppColors.navyBlue), ), ), - ), + ), ], ), ); @@ -623,16 +637,17 @@ class _DevotionalScreenState extends ConsumerState { void _showShareDialog(BuildContext context) { // 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'; + final pairingCode = + userProfile?.id.substring(0, 6).toUpperCase() ?? 'ABC123'; showDialog( context: context, builder: (context) => AlertDialog( - title: Row( + title: const Row( children: [ Icon(Icons.share_outlined, color: AppColors.navyBlue), - const SizedBox(width: 8), - const Text('Share with Husband'), + SizedBox(width: 8), + Text('Share with Husband'), ], ), content: Column( @@ -640,15 +655,17 @@ class _DevotionalScreenState extends ConsumerState { children: [ Text( 'Share this code with your husband so he can connect to your cycle data:', - style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray), + 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.navyBlue.withOpacity(0.1), + color: AppColors.navyBlue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.navyBlue.withOpacity(0.3)), + border: Border.all( + color: AppColors.navyBlue.withValues(alpha: 0.3)), ), child: SelectableText( pairingCode, @@ -663,7 +680,8 @@ class _DevotionalScreenState extends ConsumerState { 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), + style: + GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), textAlign: TextAlign.center, ), ], diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index 23e6788..57299d6 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -3,8 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../theme/app_theme.dart'; import '../../models/user_profile.dart'; -import '../../models/cycle_entry.dart'; -import '../../models/scripture.dart'; + import '../calendar/calendar_screen.dart'; import '../log/log_screen.dart'; import '../log/pad_tracker_screen.dart'; @@ -20,7 +19,7 @@ import '../settings/privacy_settings_screen.dart'; import '../settings/supplies_settings_screen.dart'; import '../settings/export_data_screen.dart'; import '../learn/wife_learn_screen.dart'; -import '../../widgets/tip_card.dart'; + import '../../widgets/cycle_ring.dart'; import '../../widgets/scripture_card.dart'; import '../../widgets/pad_tracker_card.dart'; @@ -135,7 +134,7 @@ class HomeScreen extends ConsumerWidget { color: (Theme.of(context).brightness == Brightness.dark ? Colors.black : AppColors.charcoal) - .withOpacity(0.1), + .withValues(alpha: 0.1), blurRadius: 10, offset: const Offset(0, -2), ), @@ -153,7 +152,7 @@ class HomeScreen extends ConsumerWidget { } class _DashboardTab extends ConsumerStatefulWidget { - const _DashboardTab({super.key}); + const _DashboardTab(); @override ConsumerState<_DashboardTab> createState() => _DashboardTabState(); @@ -190,8 +189,6 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> { 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; @@ -244,7 +241,7 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> { Positioned( left: 0, child: IconButton( - icon: Icon(Icons.arrow_back_ios), + icon: const Icon(Icons.arrow_back_ios), onPressed: () => ref .read(scriptureProvider.notifier) .getPreviousScripture(), @@ -254,7 +251,7 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> { Positioned( right: 0, child: IconButton( - icon: Icon(Icons.arrow_forward_ios), + icon: const Icon(Icons.arrow_forward_ios), onPressed: () => ref .read(scriptureProvider.notifier) .getNextScripture(), @@ -336,7 +333,7 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> { width: 48, height: 48, decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer.withOpacity(0.5), + color: theme.colorScheme.primaryContainer.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(12), ), child: Icon( @@ -357,7 +354,8 @@ class _SettingsTab extends ConsumerWidget { {VoidCallback? onTap}) { return ListTile( leading: Icon(icon, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8)), + color: + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.8)), title: Text( title, style: Theme.of(context).textTheme.bodyLarge?.copyWith( @@ -445,7 +443,7 @@ class _SettingsTab extends ConsumerWidget { color: Theme.of(context) .colorScheme .outline - .withOpacity(0.05)), + .withValues(alpha: 0.05)), ), child: Row( children: [ @@ -458,11 +456,11 @@ class _SettingsTab extends ConsumerWidget { Theme.of(context) .colorScheme .primary - .withOpacity(0.7), + .withValues(alpha: 0.7), Theme.of(context) .colorScheme .secondary - .withOpacity(0.7) + .withValues(alpha: 0.7) ], begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -613,7 +611,7 @@ class _SettingsTab extends ConsumerWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => CycleHistoryScreen())); + builder: (context) => const CycleHistoryScreen())); }), _buildSettingsTile( context, Icons.download_outlined, 'Export Data', onTap: () { @@ -655,7 +653,10 @@ class _SettingsTab extends ConsumerWidget { color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(12), border: Border.all( - color: Theme.of(context).colorScheme.outline.withOpacity(0.05)), + color: Theme.of(context) + .colorScheme + .outline + .withValues(alpha: 0.05)), ), child: Column( children: tiles, @@ -767,74 +768,6 @@ class _SettingsTab extends ConsumerWidget { ), ); } - - void _showShareDialog(BuildContext context, WidgetRef ref) { - // Generate a simple pairing code (in a real app, this would be stored/validated) - final userProfile = ref.read(userProfileProvider); - final pairingCode = - userProfile?.id?.substring(0, 6).toUpperCase() ?? 'ABC123'; - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Row( - children: [ - Icon(Icons.share_outlined, - color: Theme.of(context).colorScheme.primary), - const SizedBox(width: 8), - const Text('Share with Husband'), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Share this code with your husband so he can connect to your cycle data:', - style: - GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray), - ), - const SizedBox(height: 24), - Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: - Theme.of(context).colorScheme.primary.withOpacity(0.3)), - ), - child: SelectableText( - pairingCode, - style: GoogleFonts.outfit( - fontSize: 32, - fontWeight: FontWeight.bold, - letterSpacing: 4, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - const SizedBox(height: 16), - Text( - 'He can enter this in his app under Settings > Connect with Wife.', - style: - GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), - textAlign: TextAlign.center, - ), - ], - ), - actions: [ - ElevatedButton( - onPressed: () => Navigator.pop(context), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: Colors.white, - ), - child: const Text('Done'), - ), - ], - ), - ); - } } Widget _buildWifeTipsSection(BuildContext context) { @@ -900,7 +833,7 @@ Widget _buildTipCard( Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( diff --git a/lib/screens/husband/husband_appearance_screen.dart b/lib/screens/husband/husband_appearance_screen.dart index 106fe01..12f644e 100644 --- a/lib/screens/husband/husband_appearance_screen.dart +++ b/lib/screens/husband/husband_appearance_screen.dart @@ -137,7 +137,7 @@ class HusbandAppearanceScreen extends ConsumerWidget { boxShadow: [ if (isSelected) BoxShadow( - color: color.withOpacity(0.4), + color: color.withValues(alpha: 0.4), blurRadius: 8, offset: const Offset(0, 4), ) diff --git a/lib/screens/husband/husband_devotional_screen.dart b/lib/screens/husband/husband_devotional_screen.dart index c42754a..5897b32 100644 --- a/lib/screens/husband/husband_devotional_screen.dart +++ b/lib/screens/husband/husband_devotional_screen.dart @@ -8,6 +8,7 @@ import '../../providers/user_provider.dart'; import '../../theme/app_theme.dart'; import '../../services/bible_xml_parser.dart'; import '../../services/mock_data_service.dart'; +import '../../services/notification_service.dart'; class HusbandDevotionalScreen extends ConsumerStatefulWidget { const HusbandDevotionalScreen({super.key}); @@ -190,11 +191,11 @@ class _HusbandDevotionalScreenState // Trigger notification for new teaching plans if (existingPlan == null) { NotificationService().showTeachingPlanNotification( - teacherName: user.name ?? 'Husband', + teacherName: user.name, ); } - if (mounted) Navigator.pop(context); + if (context.mounted) Navigator.pop(context); }, child: const Text('Save'), ), @@ -283,7 +284,7 @@ class _HusbandDevotionalScreenState decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.grey.withOpacity(0.2)), + border: Border.all(color: Colors.grey.withValues(alpha: 0.2)), ), child: Column( children: [ @@ -314,7 +315,7 @@ class _HusbandDevotionalScreenState background: Container( alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 20), - color: Colors.red.withOpacity(0.8), + color: Colors.red.withValues(alpha: 0.8), child: const Icon(Icons.delete, color: Colors.white), ), onDismissed: (_) => _deletePlan(plan), @@ -517,14 +518,14 @@ class _HusbandDevotionalScreenState decoration: BoxDecoration( gradient: LinearGradient( colors: [ - AppColors.lavender.withOpacity(0.15), - AppColors.blushPink.withOpacity(0.15), + AppColors.lavender.withValues(alpha: 0.15), + AppColors.blushPink.withValues(alpha: 0.15), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.lavender.withOpacity(0.3)), + border: Border.all(color: AppColors.lavender.withValues(alpha: 0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -623,12 +624,12 @@ class _HusbandDevotionalScreenState width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.5), + color: Colors.white.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(12), ), child: Column( children: [ - Icon(Icons.favorite_border, + const Icon(Icons.favorite_border, color: AppColors.warmGray, size: 32), const SizedBox(height: 8), Text( @@ -642,7 +643,7 @@ class _HusbandDevotionalScreenState 'Check back later or encourage her to share.', style: GoogleFonts.outfit( fontSize: 12, - color: AppColors.warmGray.withOpacity(0.8), + color: AppColors.warmGray.withValues(alpha: 0.8), ), ), ], @@ -660,8 +661,8 @@ class _HusbandDevotionalScreenState showDialog( context: context, builder: (context) => AlertDialog( - title: Row( - children: const [ + title: const Row( + children: [ Icon(Icons.link, color: AppColors.navyBlue), SizedBox(width: 8), Text('Connect with Wife'), @@ -723,12 +724,14 @@ class _HusbandDevotionalScreenState .updateProfile(updatedProfile); } - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Connected with wife! 💑'), - backgroundColor: AppColors.sageGreen, - ), - ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Connected with wife! 💑'), + backgroundColor: AppColors.sageGreen, + ), + ); + } } }, style: ElevatedButton.styleFrom( diff --git a/lib/screens/husband/husband_home_screen.dart b/lib/screens/husband/husband_home_screen.dart index 6f504b9..7e53aa7 100644 --- a/lib/screens/husband/husband_home_screen.dart +++ b/lib/screens/husband/husband_home_screen.dart @@ -2,20 +2,15 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/user_profile.dart'; -import '../../models/teaching_plan.dart'; import '../../models/cycle_entry.dart'; import '../../models/scripture.dart'; import '../../providers/user_provider.dart'; import '../../theme/app_theme.dart'; import '../../services/notification_service.dart'; -import '../../services/mock_data_service.dart'; -import '../../services/bible_xml_parser.dart'; import '../calendar/calendar_screen.dart'; import 'husband_devotional_screen.dart'; import 'husband_settings_screen.dart'; -import 'husband_appearance_screen.dart'; -import 'learn_article_screen.dart'; -import 'husband_notes_screen.dart'; +import 'husband_learn_articles_screen.dart'; /// Husband's companion app main screen class HusbandHomeScreen extends ConsumerStatefulWidget { @@ -28,6 +23,12 @@ class HusbandHomeScreen extends ConsumerStatefulWidget { class _HusbandHomeScreenState extends ConsumerState { int _selectedIndex = 0; + void navigateToSettings() { + setState(() { + _selectedIndex = 5; + }); + } + @override Widget build(BuildContext context) { final husbandTheme = _husbandTheme; @@ -40,13 +41,13 @@ class _HusbandHomeScreenState extends ConsumerState { backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: IndexedStack( index: _selectedIndex, - children: [ - const _HusbandDashboard(), - const CalendarScreen(readOnly: true), // Reused Calendar - const HusbandDevotionalScreen(), // Devotional & Planning - const _HusbandTipsScreen(), - const _HusbandLearnScreen(), - const HusbandSettingsScreen(), + children: const [ + _HusbandDashboard(), + CalendarScreen(readOnly: true), // Reused Calendar + HusbandDevotionalScreen(), // Devotional & Planning + _HusbandTipsScreen(), + HusbandLearnArticlesScreen(), + HusbandSettingsScreen(), ], ), bottomNavigationBar: Container( @@ -55,7 +56,7 @@ class _HusbandHomeScreenState extends ConsumerState { boxShadow: [ BoxShadow( color: (isDark ? Colors.black : AppColors.navyBlue) - .withOpacity(0.1), + .withValues(alpha: 0.1), blurRadius: 10, offset: const Offset(0, -2), ), @@ -134,7 +135,6 @@ class _HusbandHomeScreenState extends ConsumerState { secondary: const Color(0xFFD4A574), // Warm gold for contrast surface: const Color(0xFF1E1E1E), onSurface: Colors.white, - onBackground: Colors.white, ), // Text theme with high contrast for dark mode textTheme: TextTheme( @@ -156,19 +156,19 @@ class _HusbandHomeScreenState extends ConsumerState { titleMedium: GoogleFonts.outfit( fontSize: 16, fontWeight: FontWeight.w500, - color: Colors.white.withOpacity(0.95), + color: Colors.white.withValues(alpha: 0.95), ), bodyLarge: GoogleFonts.outfit( fontSize: 16, - color: Colors.white.withOpacity(0.9), + color: Colors.white.withValues(alpha: 0.9), ), bodyMedium: GoogleFonts.outfit( fontSize: 14, - color: Colors.white.withOpacity(0.85), + color: Colors.white.withValues(alpha: 0.85), ), bodySmall: GoogleFonts.outfit( fontSize: 12, - color: Colors.white.withOpacity(0.7), + color: Colors.white.withValues(alpha: 0.7), ), labelLarge: GoogleFonts.outfit( fontSize: 14, @@ -195,7 +195,7 @@ class _HusbandHomeScreenState extends ConsumerState { ), ), cardColor: const Color(0xFF2A2A2A), - dividerColor: Colors.white.withOpacity(0.12), + dividerColor: Colors.white.withValues(alpha: 0.12), iconTheme: const IconThemeData(color: Colors.white70), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( @@ -223,7 +223,6 @@ class _HusbandHomeScreenState extends ConsumerState { secondary: AppColors.gold, surface: Colors.white, onSurface: AppColors.charcoal, - onBackground: AppColors.charcoal, ), textTheme: TextTheme( headlineLarge: GoogleFonts.outfit( @@ -244,7 +243,7 @@ class _HusbandHomeScreenState extends ConsumerState { titleMedium: GoogleFonts.outfit( fontSize: 16, fontWeight: FontWeight.w500, - color: AppColors.charcoal.withOpacity(0.9), + color: AppColors.charcoal.withValues(alpha: 0.9), ), bodyLarge: GoogleFonts.outfit( fontSize: 16, @@ -252,7 +251,7 @@ class _HusbandHomeScreenState extends ConsumerState { ), bodyMedium: GoogleFonts.outfit( fontSize: 14, - color: AppColors.charcoal.withOpacity(0.85), + color: AppColors.charcoal.withValues(alpha: 0.85), ), bodySmall: GoogleFonts.outfit( fontSize: 12, @@ -284,7 +283,7 @@ class _HusbandHomeScreenState extends ConsumerState { ), cardColor: Colors.white, dividerColor: AppColors.lightGray, - iconTheme: IconThemeData(color: AppColors.warmGray), + iconTheme: const IconThemeData(color: AppColors.warmGray), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: accentColor, @@ -368,7 +367,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( - gradient: LinearGradient( + gradient: const LinearGradient( colors: [ AppColors.navyBlue, AppColors.steelBlue, @@ -387,7 +386,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { width: 40, height: 40, decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), + color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(10), ), child: const Icon( @@ -402,7 +401,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { style: GoogleFonts.outfit( fontSize: 16, fontWeight: FontWeight.w500, - color: Colors.white.withOpacity(0.9), + color: Colors.white.withValues(alpha: 0.9), ), ), ], @@ -423,7 +422,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { : 'Period expected soon', style: GoogleFonts.outfit( fontSize: 14, - color: Colors.white.withOpacity(0.8), + color: Colors.white.withValues(alpha: 0.8), ), ), const SizedBox(height: 16), @@ -433,7 +432,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { vertical: 6, ), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), + color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(20), ), child: Row( @@ -465,7 +464,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: AppColors.navyBlue.withOpacity(0.05), + color: AppColors.navyBlue.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -480,7 +479,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { width: 36, height: 36, decoration: BoxDecoration( - color: AppColors.gold.withOpacity(0.2), + color: AppColors.gold.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(10), ), child: const Icon( @@ -541,10 +540,11 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.rose.withOpacity(0.3)), + border: Border.all( + color: AppColors.rose.withValues(alpha: 0.3)), boxShadow: [ BoxShadow( - color: AppColors.rose.withOpacity(0.05), + color: AppColors.rose.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -559,10 +559,10 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { width: 36, height: 36, decoration: BoxDecoration( - color: AppColors.rose.withOpacity(0.1), + color: AppColors.rose.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), - child: Icon( + child: const Icon( Icons.fastfood, color: AppColors.rose, size: 20, @@ -587,7 +587,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { .map((craving) => Chip( label: Text(craving), backgroundColor: - AppColors.rose.withOpacity(0.1), + AppColors.rose.withValues(alpha: 0.1), labelStyle: GoogleFonts.outfit( color: AppColors.navyBlue, fontWeight: FontWeight.w500), @@ -608,7 +608,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { decoration: BoxDecoration( gradient: LinearGradient( colors: [ - AppColors.gold.withOpacity(0.15), + AppColors.gold.withValues(alpha: 0.15), AppColors.warmCream, ], begin: Alignment.topLeft, @@ -616,7 +616,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { ), borderRadius: BorderRadius.circular(16), border: Border.all( - color: AppColors.gold.withOpacity(0.3), + color: AppColors.gold.withValues(alpha: 0.3), ), ), child: Column( @@ -624,7 +624,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { children: [ Row( children: [ - Icon( + const Icon( Icons.menu_book, color: AppColors.gold, size: 20, @@ -647,7 +647,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: AppColors.gold.withOpacity(0.15), + color: AppColors.gold.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), ), child: Row( @@ -662,7 +662,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { ), ), const SizedBox(width: 2), - Icon(Icons.arrow_drop_down, + const Icon(Icons.arrow_drop_down, color: AppColors.gold, size: 16), ], ), @@ -698,13 +698,13 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: AppColors.gold.withOpacity(0.1), + color: AppColors.gold.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.refresh, + const Icon(Icons.refresh, color: AppColors.gold, size: 14), const SizedBox(width: 4), Text( @@ -733,7 +733,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { onPressed: () => _showPrayerPrompt(context, phase), icon: const Text('🙏', style: TextStyle(fontSize: 18)), label: Text( - 'Pray for ${wifeName}', + 'Pray for $wifeName', style: GoogleFonts.outfit(fontWeight: FontWeight.w500), ), style: OutlinedButton.styleFrom( @@ -962,8 +962,9 @@ class _HusbandTipsScreen extends StatelessWidget { Consumer( builder: (context, ref, child) { final user = ref.watch(userProfileProvider); - if (user == null || !user.isPadTrackingEnabled) + if (user == null || !user.isPadTrackingEnabled) { return const SizedBox.shrink(); + } final brand = user.padBrand ?? 'Not specified'; final flow = user.typicalFlowIntensity; @@ -974,10 +975,11 @@ class _HusbandTipsScreen extends StatelessWidget { width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: AppColors.menstrualPhase.withOpacity(0.15), + color: AppColors.menstrualPhase.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(16), border: Border.all( - color: AppColors.menstrualPhase.withOpacity(0.3)), + color: AppColors.menstrualPhase + .withValues(alpha: 0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1043,7 +1045,7 @@ class _HusbandTipsScreen extends StatelessWidget { width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: AppColors.rose.withOpacity(0.1), + color: AppColors.rose.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.rose), ), @@ -1067,15 +1069,16 @@ class _HusbandTipsScreen extends StatelessWidget { const SizedBox(height: 4), GestureDetector( onTap: () { - // Navigate to settings - final parentState = + // Navigate to settings - usually done via a callback or provider + // For simplicity here, we can try to find the state but if it's private + // we may need a different approach. However, looking at the code, + // HusbandHomeScreen is the parent. We can use a notification or just fix the logic. + // Based on the error, let's try to notify the parent. + final state = context.findAncestorStateOfType< _HusbandHomeScreenState>(); - if (parentState != null) { - parentState.setState(() { - parentState._selectedIndex = - 5; // Settings tab - }); + if (state != null) { + state.navigateToSettings(); } }, child: Text( @@ -1122,8 +1125,9 @@ class _HusbandTipsScreen extends StatelessWidget { final user = ref.watch(userProfileProvider); final favorites = user?.favoriteFoods; - if (favorites == null || favorites.isEmpty) + if (favorites == null || favorites.isEmpty) { return const SizedBox.shrink(); + } return Column( children: [ @@ -1191,841 +1195,3 @@ class _HusbandTipsScreen extends StatelessWidget { ); } } - -class _HusbandLearnScreen extends StatelessWidget { - const _HusbandLearnScreen(); - - @override - Widget build(BuildContext context) { - return SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Learn', - style: GoogleFonts.outfit( - fontSize: 28, - fontWeight: FontWeight.w600, - color: AppColors.navyBlue, - ), - ), - const SizedBox(height: 24), - _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(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(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', - ), - ]), - ], - ), - ), - ); - } - - Widget _buildSection( - BuildContext context, String title, List<_LearnItem> items) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: GoogleFonts.outfit( - fontSize: 14, - fontWeight: FontWeight.w500, - color: AppColors.warmGray, - letterSpacing: 0.5, - ), - ), - const SizedBox(height: 8), - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - children: items - .map((item) => ListTile( - leading: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: AppColors.navyBlue.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - item.icon, - color: AppColors.navyBlue, - size: 20, - ), - ), - title: Text( - item.title, - style: GoogleFonts.outfit( - fontSize: 15, - fontWeight: FontWeight.w500, - color: AppColors.charcoal, - ), - ), - subtitle: Text( - item.subtitle, - style: GoogleFonts.outfit( - fontSize: 13, - color: AppColors.warmGray, - ), - ), - trailing: const Icon( - Icons.chevron_right, - color: AppColors.lightGray, - ), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - LearnArticleScreen(articleId: item.articleId), - ), - ); - }, - )) - .toList(), - ), - ), - ], - ); - } -} - -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, - }); -} - -class _HusbandSettingsScreen extends ConsumerWidget { - const _HusbandSettingsScreen(); - - Future _resetApp(BuildContext context, WidgetRef ref) async { - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Reset App?'), - content: const Text( - 'This will clear all data and return you to onboarding. Are you sure?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel')), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: const Text('Reset', style: TextStyle(color: Colors.red)), - ), - ], - ), - ); - - if (confirmed == true) { - await ref.read(userProfileProvider.notifier).clearProfile(); - await ref.read(cycleEntriesProvider.notifier).clearEntries(); - - if (context.mounted) { - Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false); - } - } - } - - Future _loadDemoData(BuildContext context, WidgetRef ref) async { - final confirmed = await showDialog( - 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(); - bool shareDevotional = true; - - showDialog( - context: context, - builder: (context) => StatefulBuilder( - builder: (context, setState) => 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), - ), - const SizedBox(height: 24), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 24, - width: 24, - child: Checkbox( - value: shareDevotional, - onChanged: (val) => - setState(() => shareDevotional = val ?? true), - activeColor: AppColors.navyBlue, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Share Devotional Plans', - style: GoogleFonts.outfit( - fontWeight: FontWeight.bold, - fontSize: 14, - color: AppColors.charcoal), - ), - Text( - 'Allow her to see the teaching plans you create.', - style: GoogleFonts.outfit( - fontSize: 12, color: AppColors.warmGray), - ), - ], - ), - ), - ], - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), - ), - ElevatedButton( - onPressed: () async { - final code = codeController.text.trim(); - - Navigator.pop(context); - - // Update preference - final user = ref.read(userProfileProvider); - if (user != null) { - final updatedProfile = - user.copyWith(isDataShared: shareDevotional); - await ref - .read(userProfileProvider.notifier) - .updateProfile(updatedProfile); - } - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Settings updated & Connected!'), - backgroundColor: AppColors.sageGreen, - ), - ); - - if (code.isNotEmpty) { - // Load demo data as simulation - final mockService = MockDataService(); - final entries = mockService.generateMockCycleEntries(); - for (var entry in entries) { - await ref - .read(cycleEntriesProvider.notifier) - .addEntry(entry); - } - final mockWife = mockService.generateMockWifeProfile(); - final currentProfile = ref.read(userProfileProvider); - if (currentProfile != null) { - final updatedProfile = currentProfile.copyWith( - isDataShared: shareDevotional, - partnerName: mockWife.name, - averageCycleLength: mockWife.averageCycleLength, - averagePeriodLength: mockWife.averagePeriodLength, - lastPeriodStartDate: mockWife.lastPeriodStartDate, - favoriteFoods: mockWife.favoriteFoods, - ); - await ref - .read(userProfileProvider.notifier) - .updateProfile(updatedProfile); - } - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.navyBlue, - foregroundColor: Colors.white, - ), - child: const Text('Connect'), - ), - ], - ), - ), - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Settings', - style: GoogleFonts.outfit( - fontSize: 28, - fontWeight: FontWeight.w600, - color: AppColors.navyBlue, - ), - ), - const SizedBox(height: 24), - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - children: [ - ListTile( - 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, color: AppColors.navyBlue), - title: Text('Connect with Wife', - style: GoogleFonts.outfit(fontWeight: FontWeight.w500)), - trailing: const Icon(Icons.chevron_right), - onTap: () => _showConnectDialog(context, ref), - ), - const Divider(height: 1), - ListTile( - 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 Divider(height: 1), - ListTile( - leading: const Icon(Icons.palette_outlined, - color: AppColors.navyBlue), - title: Text('Appearance', - style: GoogleFonts.outfit(fontWeight: FontWeight.w500)), - trailing: const Icon(Icons.chevron_right), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const HusbandAppearanceScreen(), - ), - ); - }, - ), - ], - ), - ), - 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), - ), - ], - ), - ), - ], - ), - ), - ); - } -} - -class _HusbandWifeStatus extends ConsumerWidget { - const _HusbandWifeStatus(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final user = ref.watch(userProfileProvider); - final cycleInfo = ref.watch(currentCycleInfoProvider); - final entries = ref.watch(cycleEntriesProvider); - - final wifeName = user?.partnerName ?? "Wife"; - final phase = cycleInfo.phase; - final dayOfCycle = cycleInfo.dayOfCycle; - - // Find today's entry - final todayEntry = entries.firstWhere( - (e) => DateUtils.isSameDay(e.date, DateTime.now()), - orElse: () => CycleEntry( - id: '', - date: DateTime.now(), - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ), - ); - - return SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Wife\'s Status', - style: GoogleFonts.outfit( - fontSize: 28, - fontWeight: FontWeight.w600, - color: AppColors.navyBlue, - ), - ), - const SizedBox(height: 8), - Text( - 'Real-time updates on how $wifeName is doing', - style: GoogleFonts.outfit( - fontSize: 14, - color: AppColors.warmGray, - ), - ), - const SizedBox(height: 24), - - // Phase and Day summary - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: AppColors.navyBlue.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Row( - children: [ - _buildStatusCircle(dayOfCycle, phase), - const SizedBox(width: 20), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - phase.label, - style: GoogleFonts.outfit( - fontSize: 20, - fontWeight: FontWeight.w600, - color: AppColors.navyBlue, - ), - ), - Text( - 'Cycle Day $dayOfCycle', - style: GoogleFonts.outfit( - fontSize: 14, - color: AppColors.warmGray, - ), - ), - const SizedBox(height: 8), - Text( - phase.description, - style: GoogleFonts.outfit( - fontSize: 13, - color: AppColors.charcoal.withOpacity(0.8), - ), - ), - ], - ), - ), - ], - ), - ), - const SizedBox(height: 24), - - // Symptoms for Today - if (todayEntry.hasSymptoms || todayEntry.mood != null) ...[ - Text( - 'Today\'s Logs', - style: GoogleFonts.outfit( - fontSize: 18, - fontWeight: FontWeight.w600, - color: AppColors.navyBlue, - ), - ), - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.navyBlue.withOpacity(0.03), - borderRadius: BorderRadius.circular(16), - 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}'), - if (todayEntry.hasSymptoms) - _buildLogTile(Icons.healing_outlined, 'Symptoms', - _getSymptomsSummary(todayEntry)), - if (todayEntry.energyLevel != null) - _buildLogTile(Icons.flash_on, 'Energy', - '${todayEntry.energyLevel}/5'), - ], - ), - ), - const SizedBox(height: 24), - ], - - // Support Checklist - Text( - 'Support Checklist', - style: GoogleFonts.outfit( - fontSize: 18, - fontWeight: FontWeight.w600, - color: AppColors.navyBlue, - ), - ), - const SizedBox(height: 12), - ..._generateChecklist(todayEntry, phase) - .map((item) => _buildCheckItem(item)), - - const SizedBox(height: 40), - ], - ), - ), - ); - } - - Widget _buildStatusCircle(int day, CyclePhase phase) { - return Container( - width: 70, - height: 70, - decoration: BoxDecoration( - color: phase.color.withOpacity(0.15), - shape: BoxShape.circle, - border: Border.all(color: phase.color.withOpacity(0.3), width: 2), - ), - child: Center( - child: Text( - day.toString(), - style: GoogleFonts.outfit( - fontSize: 24, - fontWeight: FontWeight.w700, - color: phase.color, - ), - ), - ), - ); - } - - Widget _buildLogTile(IconData icon, String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - children: [ - Icon(icon, size: 20, color: AppColors.steelBlue), - const SizedBox(width: 12), - Text( - '$label: ', - style: - GoogleFonts.outfit(fontWeight: FontWeight.w500, fontSize: 14), - ), - Expanded( - child: Text( - value, - style: - GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal), - ), - ), - ], - ), - ); - } - - Widget _buildCheckItem(String text) { - return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.navyBlue.withOpacity(0.05)), - ), - child: Row( - children: [ - Icon(Icons.check_circle_outline, - color: AppColors.sageGreen, size: 20), - const SizedBox(width: 12), - Expanded( - child: Text( - text, - style: - GoogleFonts.outfit(fontSize: 14, color: AppColors.navyBlue), - ), - ), - ], - ), - ); - } - - String _getSymptomsSummary(CycleEntry entry) { - List s = []; - 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'); - if (entry.hasLowerBackPain) s.add('Back Pain'); - return s.isNotEmpty ? s.join(', ') : 'None'; - } - - List _generateChecklist(CycleEntry entry, CyclePhase phase) { - List list = []; - - // Symptom-based tips - if (entry.crampIntensity != null && entry.crampIntensity! >= 3) { - list.add('Bring her a heating pad or hot water bottle.'); - } - if (entry.hasHeadache) { - list.add('Suggest some quiet time with dimmed lights.'); - } - 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) { - list.add('Offer a listening ear and extra comfort.'); - } - - // Phase-based fallback tips - if (list.length < 3) { - switch (phase) { - case CyclePhase.menstrual: - list.add('Suggest a relaxing movie night.'); - list.add('Bring her a warm tea or cocoa.'); - break; - case CyclePhase.follicular: - list.add('Plan a fun outdoor activity.'); - list.add('Compliment her renewed energy.'); - break; - case CyclePhase.ovulation: - list.add('Plan a romantic date night.'); - list.add('Focus on quality connection time.'); - break; - case CyclePhase.luteal: - list.add('Surprise her with her favorite comfort snack.'); - list.add('Be extra patient if she\'s easily frustrated.'); - break; - } - } - - return list.take(4).toList(); - } -} diff --git a/lib/screens/husband/_HusbandLearnScreen.dart b/lib/screens/husband/husband_learn_articles_screen.dart similarity index 51% rename from lib/screens/husband/_HusbandLearnScreen.dart rename to lib/screens/husband/husband_learn_articles_screen.dart index a1666cc..2bc29ef 100644 --- a/lib/screens/husband/_HusbandLearnScreen.dart +++ b/lib/screens/husband/husband_learn_articles_screen.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import '../../theme/app_theme.dart'; -import './learn_article_screen.dart'; +import '../../data/learn_content.dart'; -class _HusbandLearnScreen extends StatelessWidget { - const _HusbandLearnScreen(); +class HusbandLearnArticlesScreen extends StatelessWidget { + const HusbandLearnArticlesScreen({super.key}); @override Widget build(BuildContext context) { @@ -23,7 +22,7 @@ class _HusbandLearnScreen extends StatelessWidget { ), ), const SizedBox(height: 24), - _buildSection(context, 'Understanding Her', [ + _buildSection(context, 'Understanding Her', const [ _LearnItem( icon: Icons.loop, title: 'The 4 Phases of Her Cycle', @@ -44,7 +43,7 @@ class _HusbandLearnScreen extends StatelessWidget { ), ]), const SizedBox(height: 24), - _buildSection(context, 'Biblical Manhood', [ + _buildSection(context, 'Biblical Manhood', const [ _LearnItem( icon: Icons.favorite, title: 'Loving Like Christ', @@ -65,7 +64,7 @@ class _HusbandLearnScreen extends StatelessWidget { ), ]), const SizedBox(height: 24), - _buildSection(context, 'NFP for Husbands', [ + _buildSection(context, 'NFP for Husbands', const [ _LearnItem( icon: Icons.show_chart, title: 'Reading the Charts Together', @@ -85,7 +84,8 @@ class _HusbandLearnScreen extends StatelessWidget { ); } - Widget _buildSection(BuildContext context, String title, List<_LearnItem> items) { + Widget _buildSection( + BuildContext context, String title, List<_LearnItem> items) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -111,7 +111,10 @@ class _HusbandLearnScreen extends StatelessWidget { width: 40, height: 40, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + color: Theme.of(context) + .colorScheme + .primary + .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Icon( @@ -143,7 +146,9 @@ class _HusbandLearnScreen extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => LearnArticleScreen(articleId: item.articleId), + builder: (context) => + CombinedLearnArticleDetailScreen( + articleId: item.articleId), ), ); }, @@ -168,4 +173,148 @@ class _LearnItem { required this.subtitle, required this.articleId, }); -} \ No newline at end of file +} + +class CombinedLearnArticleDetailScreen extends StatelessWidget { + final String articleId; + + const CombinedLearnArticleDetailScreen({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: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + elevation: 0, + leading: IconButton( + icon: + Icon(Icons.arrow_back, color: Theme.of(context).iconTheme.color), + onPressed: () => Navigator.pop(context), + ), + title: Text( + article.category, + style: GoogleFonts.outfit( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + centerTitle: true, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + article.title, + style: GoogleFonts.outfit( + fontSize: 26, + fontWeight: FontWeight.w700, + color: Theme.of(context).textTheme.headlineMedium?.color, + height: 1.2, + ), + ), + const SizedBox(height: 8), + Text( + article.subtitle, + style: GoogleFonts.outfit( + fontSize: 15, + color: Theme.of(context).textTheme.bodyMedium?.color, + ), + ), + const SizedBox(height: 24), + Container( + height: 3, + width: 40, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 24), + ...article.sections + .map((section) => _buildSection(context, section)), + ], + ), + ), + ); + } + + Widget _buildSection(BuildContext context, 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: Theme.of(context).textTheme.titleLarge?.color, + ), + ), + const SizedBox(height: 10), + ], + _buildRichText(context, section.content), + ], + ), + ); + } + + Widget _buildRichText(BuildContext context, String content) { + final List spans = []; + final RegExp boldPattern = RegExp(r'\*\*(.*?)\*\*'); + + int currentIndex = 0; + for (final match in boldPattern.allMatches(content)) { + if (match.start > currentIndex) { + spans.add(TextSpan( + text: content.substring(currentIndex, match.start), + style: GoogleFonts.outfit( + fontSize: 15, + color: Theme.of(context).textTheme.bodyLarge?.color, + height: 1.7, + ), + )); + } + spans.add(TextSpan( + text: match.group(1), + style: GoogleFonts.outfit( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Theme.of(context).textTheme.titleMedium?.color, + height: 1.7, + ), + )); + currentIndex = match.end; + } + + if (currentIndex < content.length) { + spans.add(TextSpan( + text: content.substring(currentIndex), + style: GoogleFonts.outfit( + fontSize: 15, + color: Theme.of(context).textTheme.bodyLarge?.color, + height: 1.7, + ), + )); + } + + return RichText( + text: TextSpan(children: spans), + ); + } +} diff --git a/lib/screens/husband/husband_notes_screen.dart b/lib/screens/husband/husband_notes_screen.dart index 503bca6..6efa01e 100644 --- a/lib/screens/husband/husband_notes_screen.dart +++ b/lib/screens/husband/husband_notes_screen.dart @@ -2,7 +2,6 @@ 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 { @@ -16,7 +15,7 @@ class HusbandNotesScreen extends ConsumerWidget { (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)); @@ -33,7 +32,8 @@ class HusbandNotesScreen extends ConsumerWidget { itemBuilder: (context, index) { final entry = notesEntries[index]; return Card( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + margin: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -41,9 +41,10 @@ class HusbandNotesScreen extends ConsumerWidget { children: [ Text( DateFormat.yMMMMd().format(entry.date), - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + style: + Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), ), const SizedBox(height: 12), if (entry.notes != null && entry.notes!.isNotEmpty) @@ -51,7 +52,8 @@ class HusbandNotesScreen extends ConsumerWidget { title: 'Her Notes', content: entry.notes!, ), - if (entry.husbandNotes != null && entry.husbandNotes!.isNotEmpty) + if (entry.husbandNotes != null && + entry.husbandNotes!.isNotEmpty) _NoteSection( title: 'Your Notes', content: entry.husbandNotes!, @@ -93,4 +95,4 @@ class _NoteSection extends StatelessWidget { ], ); } -} \ No newline at end of file +} diff --git a/lib/screens/husband/husband_settings_screen.dart b/lib/screens/husband/husband_settings_screen.dart index e46fafd..32303e0 100644 --- a/lib/screens/husband/husband_settings_screen.dart +++ b/lib/screens/husband/husband_settings_screen.dart @@ -147,11 +147,11 @@ class HusbandSettingsScreen extends ConsumerWidget { context: context, builder: (context) => StatefulBuilder( builder: (context, setState) => AlertDialog( - title: Row( + title: const Row( children: [ - const Icon(Icons.link, color: AppColors.navyBlue), - const SizedBox(width: 8), - const Text('Connect with Wife'), + Icon(Icons.link, color: AppColors.navyBlue), + SizedBox(width: 8), + Text('Connect with Wife'), ], ), content: Column( @@ -188,7 +188,7 @@ class HusbandSettingsScreen extends ConsumerWidget { value: shareDevotional, onChanged: (val) => setState(() => shareDevotional = val ?? true), - activeColor: AppColors.navyBlue, + activeColor: AppColors.sageGreen, ), ), const SizedBox(width: 12), @@ -233,12 +233,14 @@ class HusbandSettingsScreen extends ConsumerWidget { user.copyWith(isDataShared: shareDevotional)); } - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Settings updated & Connected!'), - backgroundColor: AppColors.sageGreen, - ), - ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Settings updated & Connected!'), + backgroundColor: AppColors.sageGreen, + ), + ); + } if (code.isNotEmpty) { // Load demo data as simulation @@ -281,7 +283,7 @@ class HusbandSettingsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // Theme aware colors - final isDark = Theme.of(context).brightness == Brightness.dark; + final cardColor = Theme.of(context).cardTheme.color; // Using theme card color final textColor = Theme.of(context).textTheme.bodyLarge?.color; diff --git a/lib/screens/husband/learn_article_screen.dart b/lib/screens/husband/learn_article_screen.dart deleted file mode 100644 index 806e4f2..0000000 --- a/lib/screens/husband/learn_article_screen.dart +++ /dev/null @@ -1,156 +0,0 @@ -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: Theme.of(context).scaffoldBackgroundColor, - appBar: AppBar( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - elevation: 0, - leading: IconButton( - icon: Icon(Icons.arrow_back, color: Theme.of(context).iconTheme.color), - onPressed: () => Navigator.pop(context), - ), - title: Text( - article.category, - style: GoogleFonts.outfit( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context).textTheme.bodySmall?.color, - ), - ), - 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: Theme.of(context).textTheme.headlineMedium?.color, - height: 1.2, - ), - ), - const SizedBox(height: 8), - Text( - article.subtitle, - style: GoogleFonts.outfit( - fontSize: 15, - color: Theme.of(context).textTheme.bodyMedium?.color, - ), - ), - const SizedBox(height: 24), - - // Divider - Container( - height: 3, - width: 40, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(2), - ), - ), - const SizedBox(height: 24), - - // Sections - ...article.sections.map((section) => _buildSection(context, section)), - ], - ), - ), - ); - } - - Widget _buildSection(BuildContext context, 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: Theme.of(context).textTheme.titleLarge?.color, - ), - ), - const SizedBox(height: 10), - ], - _buildRichText(context, section.content), - ], - ), - ); - } - - Widget _buildRichText(BuildContext context, String content) { - // Handle basic markdown-like formatting - final List 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: Theme.of(context).textTheme.bodyLarge?.color, - height: 1.7, - ), - )); - } - // Add bold text - spans.add(TextSpan( - text: match.group(1), - style: GoogleFonts.outfit( - fontSize: 15, - fontWeight: FontWeight.w600, - color: Theme.of(context).textTheme.titleMedium?.color, - 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: Theme.of(context).textTheme.bodyLarge?.color, - height: 1.7, - ), - )); - } - - return RichText( - text: TextSpan(children: spans), - ); - } -} diff --git a/lib/screens/learn/husband_learn_screen.dart b/lib/screens/learn/husband_learn_screen.dart index b447b96..600ae1c 100644 --- a/lib/screens/learn/husband_learn_screen.dart +++ b/lib/screens/learn/husband_learn_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:christian_period_tracker/models/scripture.dart'; class HusbandLearnScreen extends StatelessWidget { const HusbandLearnScreen({super.key}); @@ -45,7 +44,8 @@ class HusbandLearnScreen extends StatelessWidget { const SizedBox(height: 16), Card( elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -54,21 +54,24 @@ class HusbandLearnScreen extends StatelessWidget { _buildTipCard( context, title: 'Understanding Female Cycles', - content: 'Learn about the different phases of your wife\'s menstrual cycle and how they affect her health.', + content: + 'Learn about the different phases of your wife\'s menstrual cycle and how they affect her health.', icon: Icons.calendar_month, ), const SizedBox(height: 16), _buildTipCard( context, title: 'Supportive Role', - content: 'Be supportive during different phases, understanding when she may need more emotional support or rest.', + content: + 'Be supportive during different phases, understanding when she may need more emotional support or rest.', icon: Icons.support_agent, ), const SizedBox(height: 16), _buildTipCard( context, title: 'Nutritional Support', - content: 'Understand how nutrition affects reproductive health and discuss dietary choices together.', + content: + 'Understand how nutrition affects reproductive health and discuss dietary choices together.', icon: Icons.food_bank, ), ], @@ -90,7 +93,8 @@ class HusbandLearnScreen extends StatelessWidget { const SizedBox(height: 16), Card( elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -99,21 +103,24 @@ class HusbandLearnScreen extends StatelessWidget { _buildTipCard( context, title: 'Safe Sexual Practices', - content: 'Use protection consistently to prevent sexually transmitted infections and maintain mutual health.', + content: + 'Use protection consistently to prevent sexually transmitted infections and maintain mutual health.', icon: Icons.security, ), const SizedBox(height: 16), _buildTipCard( context, title: 'Regular Testing', - content: 'Schedule regular STI screenings together with your partner for early detection and treatment.', + content: + 'Schedule regular STI screenings together with your partner for early detection and treatment.', icon: Icons.medical_information, ), const SizedBox(height: 16), _buildTipCard( context, title: 'Open Communication', - content: 'Discuss health concerns openly to ensure both partners understand each other\'s needs and maintain trust.', + content: + 'Discuss health concerns openly to ensure both partners understand each other\'s needs and maintain trust.', icon: Icons.chat, ), ], @@ -135,7 +142,8 @@ class HusbandLearnScreen extends StatelessWidget { const SizedBox(height: 16), Card( elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -144,14 +152,16 @@ class HusbandLearnScreen extends StatelessWidget { _buildTipCard( context, title: 'Educate Yourself', - content: 'Take the initiative to learn about reproductive health and partner needs.', + content: + 'Take the initiative to learn about reproductive health and partner needs.', icon: Icons.school, ), const SizedBox(height: 16), _buildTipCard( context, title: 'Active Listening', - content: 'Listen attentively when your wife discusses her health concerns or feelings.', + content: + 'Listen attentively when your wife discusses her health concerns or feelings.', icon: Icons.mic_none, ), ], @@ -162,7 +172,10 @@ class HusbandLearnScreen extends StatelessWidget { ); } - Widget _buildTipCard(BuildContext context, {required String title, required String content, required IconData icon}) { + Widget _buildTipCard(BuildContext context, + {required String title, + required String content, + required IconData icon}) { return Card( elevation: 1, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), @@ -174,10 +187,14 @@ class HusbandLearnScreen extends StatelessWidget { width: 40, height: 40, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.2), + color: Theme.of(context) + .colorScheme + .primaryContainer + .withValues(alpha: 0.2), shape: BoxShape.circle, ), - child: Icon(icon, size: 24, color: Theme.of(context).colorScheme.primary), + child: Icon(icon, + size: 24, color: Theme.of(context).colorScheme.primary), ), const SizedBox(width: 16), Expanded( @@ -186,7 +203,10 @@ class HusbandLearnScreen extends StatelessWidget { children: [ Text( title, - style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 4), Text( @@ -201,4 +221,4 @@ class HusbandLearnScreen extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/screens/learn/wife_learn_screen.dart b/lib/screens/learn/wife_learn_screen.dart index 6f52dad..d120ce6 100644 --- a/lib/screens/learn/wife_learn_screen.dart +++ b/lib/screens/learn/wife_learn_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../theme/app_theme.dart'; -import '../husband/learn_article_screen.dart'; +import '../husband/husband_learn_articles_screen.dart'; class WifeLearnScreen extends StatelessWidget { const WifeLearnScreen({super.key}); @@ -23,7 +23,7 @@ class WifeLearnScreen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSection(context, 'Understanding My Cycle', [ + _buildSection(context, 'Understanding My Cycle', const [ _LearnItem( icon: Icons.loop, title: 'The 4 Phases', @@ -34,11 +34,12 @@ class WifeLearnScreen extends StatelessWidget { icon: Icons.psychology_outlined, title: 'Mood & Hormones', subtitle: 'Why I feel different each week', - articleId: 'wife_mood_changes', // Reusing similar concept, maybe new article + articleId: + 'wife_mood_changes', // Reusing similar concept, maybe new article ), ]), const SizedBox(height: 24), - _buildSection(context, 'Disease Prevention', [ + _buildSection(context, 'Disease Prevention', const [ _LearnItem( icon: Icons.health_and_safety_outlined, title: 'Preventing Infections', @@ -53,7 +54,7 @@ class WifeLearnScreen extends StatelessWidget { ), ]), const SizedBox(height: 24), - _buildSection(context, 'Partnership', [ + _buildSection(context, 'Partnership', const [ _LearnItem( icon: Icons.favorite_border, title: 'Communication', @@ -73,7 +74,8 @@ class WifeLearnScreen extends StatelessWidget { ); } - Widget _buildSection(BuildContext context, String title, List<_LearnItem> items) { + Widget _buildSection( + BuildContext context, String title, List<_LearnItem> items) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -92,7 +94,10 @@ class WifeLearnScreen extends StatelessWidget { color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(12), border: Border.all( - color: Theme.of(context).colorScheme.outline.withOpacity(0.05)), + color: Theme.of(context) + .colorScheme + .outline + .withValues(alpha: 0.05)), ), child: Column( children: items @@ -101,7 +106,10 @@ class WifeLearnScreen extends StatelessWidget { width: 40, height: 40, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.2), + color: Theme.of(context) + .colorScheme + .primaryContainer + .withValues(alpha: 0.2), borderRadius: BorderRadius.circular(10), ), child: Icon( @@ -133,7 +141,9 @@ class WifeLearnScreen extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => LearnArticleScreen(articleId: item.articleId), + builder: (context) => + CombinedLearnArticleDetailScreen( + articleId: item.articleId), ), ); }, @@ -158,4 +168,4 @@ class _LearnItem { required this.subtitle, required this.articleId, }); -} \ No newline at end of file +} diff --git a/lib/screens/log/log_screen.dart b/lib/screens/log/log_screen.dart index 8b572f3..4e268c9 100644 --- a/lib/screens/log/log_screen.dart +++ b/lib/screens/log/log_screen.dart @@ -41,17 +41,19 @@ class _LogScreenState extends ConsumerState { int? _stressLevel; final TextEditingController _notesController = TextEditingController(); final TextEditingController _cravingsController = TextEditingController(); - final TextEditingController _pantylinerCountController = TextEditingController(); - + final TextEditingController _pantylinerCountController = + TextEditingController(); + // Intimacy tracking bool _hadIntimacy = false; - bool? _intimacyProtected; // null = no selection, true = protected, false = unprotected - + bool? + _intimacyProtected; // null = no selection, true = protected, false = unprotected + // Pantyliner / Supply tracking bool _usedPantyliner = false; // Used for "Did you use supplies?" int _pantylinerCount = 0; int? _selectedSupplyIndex; // Index of selected supply from inventory - + // Hidden field to preserve husband's notes String? _husbandNotes; @@ -125,7 +127,9 @@ class _LogScreenState extends ConsumerState { id: _existingEntryId ?? const Uuid().v4(), date: _selectedDate, isPeriodDay: _isPeriodDay, - flowIntensity: _isPeriodDay ? _flowIntensity : (_isSpotting ? FlowIntensity.spotting : null), + flowIntensity: _isPeriodDay + ? _flowIntensity + : (_isSpotting ? FlowIntensity.spotting : null), mood: _mood, energyLevel: _energyLevel, crampIntensity: _crampIntensity > 0 ? _crampIntensity : null, @@ -157,21 +161,20 @@ class _LogScreenState extends ConsumerState { } // Trigger Notification if Period Start - if (_isPeriodDay && ref.read(userProfileProvider)?.notifyPeriodStart == true) { - // Check if this is likely Day 1 (simplified check: no period yesterday) - // meaningful logic requires checking previous entry, but for now we trust the user logging "Is today a period day?" - // better: check if *yesterday* was NOT a period day. - final entries = ref.read(cycleEntriesProvider); - final yesterday = _selectedDate.subtract(const Duration(days: 1)); - final wasPeriodYesterday = entries.any((e) => DateUtils.isSameDay(e.date, yesterday) && e.isPeriodDay); - - if (!wasPeriodYesterday) { - NotificationService().showLocalNotification( - id: 1001, - title: 'Period Started', - body: 'Period start recorded for ${_formatDate(_selectedDate)}.', - ); - } + if (_isPeriodDay && + ref.read(userProfileProvider)?.notifyPeriodStart == true) { + final entries = ref.read(cycleEntriesProvider); + final yesterday = _selectedDate.subtract(const Duration(days: 1)); + final wasPeriodYesterday = entries + .any((e) => DateUtils.isSameDay(e.date, yesterday) && e.isPeriodDay); + + if (!wasPeriodYesterday) { + NotificationService().showLocalNotification( + id: 1001, + title: 'Period Started', + body: 'Period start recorded for ${_formatDate(_selectedDate)}.', + ); + } } if (mounted) { @@ -230,16 +233,13 @@ class _LogScreenState extends ConsumerState { if (!DateUtils.isSameDay(_selectedDate, DateTime.now())) return false; final cycleInfo = CycleService.calculateCycleInfo(user, entries); - - // If we are in menstrual phase and near the end (Day 3+) - // or if the cycle info thinks we are just past it but we haven't logged today. + return cycleInfo.phase == CyclePhase.menstrual && cycleInfo.dayOfCycle >= 3; } @override Widget build(BuildContext context) { final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; final userProfile = ref.watch(userProfileProvider); final isPadTrackingEnabled = userProfile?.isPadTrackingEnabled ?? false; @@ -253,53 +253,84 @@ class _LogScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'How are you feeling?', - style: GoogleFonts.outfit( - fontSize: 28, - fontWeight: FontWeight.w600, - color: theme.colorScheme.onSurface, - ), - ), - Text( - _formatDate(_selectedDate), - style: GoogleFonts.outfit( - fontSize: 14, - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - ), - if (widget.initialDate == null) - IconButton( - onPressed: () => - ref.read(navigationProvider.notifier).setIndex(0), - icon: const Icon(Icons.close), - style: IconButton.styleFrom( - backgroundColor: - theme.colorScheme.surfaceVariant.withOpacity(0.5), + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'How are you feeling?', + style: GoogleFonts.outfit( + fontSize: 28, + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, ), ), + Text( + _formatDate(_selectedDate), + style: GoogleFonts.outfit( + fontSize: 14, + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + if (widget.initialDate == null) + IconButton( + onPressed: () => + ref.read(navigationProvider.notifier).setIndex(0), + icon: const Icon(Icons.close), + style: IconButton.styleFrom( + backgroundColor: theme + .colorScheme.surfaceContainerHighest + .withValues(alpha: 0.5), + ), + ), + ], + ), + const SizedBox(height: 24), + + // Period Toggle + _buildSectionCard( + context, + title: 'Period', + child: Row( + children: [ + Expanded( + child: Text( + 'Did you start your period today?', + style: GoogleFonts.outfit( + fontSize: 16, + color: theme.colorScheme.onSurface, + ), + ), + ), + _buildYesNoControl( + context, + value: _isPeriodDay, + onChanged: (value) => setState(() { + _isPeriodDay = value; + if (value) _isSpotting = false; + }), + activeColor: AppColors.menstrualPhase, + ), ], ), - const SizedBox(height: 24), - - // Period Toggle + ), + + // Are you spotting? (only if NOT period day) + if (!_isPeriodDay) ...[ + const SizedBox(height: 16), _buildSectionCard( context, - title: 'Period', + title: 'Spotting', child: Row( children: [ Expanded( child: Text( - 'Did you start your period today?', + 'Are you spotting?', style: GoogleFonts.outfit( fontSize: 16, color: theme.colorScheme.onSurface, @@ -308,698 +339,645 @@ class _LogScreenState extends ConsumerState { ), _buildYesNoControl( context, - value: _isPeriodDay, - onChanged: (value) => setState(() { - _isPeriodDay = value; - if (value) _isSpotting = false; - }), + value: _isSpotting, + onChanged: (value) => + setState(() => _isSpotting = value), activeColor: AppColors.menstrualPhase, ), ], ), ), - - // Are you spotting? (only if NOT period day) - if (!_isPeriodDay) ...[ - const SizedBox(height: 16), - _buildSectionCard( - context, - title: 'Spotting', - child: Row( - children: [ - Expanded( - child: Text( - 'Are you spotting?', + ], + + // Still on Period? (If predicted but toggle is NO) + if (!_isPeriodDay && _shouldShowPeriodCompletionPrompt()) ...[ + const SizedBox(height: 16), + _buildSectionCard( + context, + title: 'Period Status', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Predicted period end is near. Is your period still going, or did it finish?', + style: GoogleFonts.outfit(fontSize: 14), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => + setState(() => _isPeriodDay = true), + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.menstrualPhase, + side: const BorderSide( + color: AppColors.menstrualPhase), + ), + child: const Text('Still Going'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () { + setState(() { + _isPeriodDay = false; + _isSpotting = false; + }); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('Period marked as finished.')), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.menstrualPhase, + foregroundColor: Colors.white, + ), + child: const Text('Finished'), + ), + ), + ], + ), + ], + ), + ), + ], + + // Flow Intensity (only if period day) + if (_isPeriodDay) ...[ + const SizedBox(height: 16), + _buildSectionCard( + context, + title: 'Flow Intensity', + child: Column( + children: [ + Row( + children: FlowIntensity.values.map((flow) { + final isSelected = _flowIntensity == flow; + return Expanded( + child: GestureDetector( + onTap: () => + setState(() => _flowIntensity = flow), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: + const EdgeInsets.symmetric(horizontal: 4), + padding: + const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: isSelected + ? AppColors.menstrualPhase + .withValues(alpha: 0.2) + : theme + .colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + flow.toString().split('.').last, + style: theme.textTheme.labelLarge!.copyWith( + color: isSelected + ? AppColors.menstrualPhase + : theme.colorScheme.onSurface, + fontWeight: isSelected + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ), + ), + ), + ); + }).toList(), + ), + ], + ), + ), + ], + + // Supply / Material Tracking + if (isPadTrackingEnabled) ...[ + const SizedBox(height: 16), + _buildSectionCard( + context, + title: 'Supplies', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PadTrackerScreen( + isSpotting: _isSpotting, + initialFlow: _flowIntensity, + ), + ), + ); + }, + icon: const Icon(Icons.timer_outlined), + label: const Text('Pad Tracker & Reminders'), + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.menstrualPhase, + side: const BorderSide( + color: AppColors.menstrualPhase), + ), + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: Text( + _getSupplyQuestionLabel(userProfile), + style: GoogleFonts.outfit( + fontSize: 16, + color: theme.colorScheme.onSurface, + ), + ), + ), + _buildYesNoControl( + context, + value: _usedPantyliner, + onChanged: (value) => setState(() { + _usedPantyliner = value; + if (!value) { + _pantylinerCount = 0; + _selectedSupplyIndex = null; + } + }), + activeColor: AppColors.menstrualPhase, + ), + ], + ), + if (_usedPantyliner) ...[ + const SizedBox(height: 12), + if (userProfile?.padSupplies?.isNotEmpty == true) ...[ + Text( + 'Select item from inventory:', style: GoogleFonts.outfit( - fontSize: 16, + fontSize: 14, + fontWeight: FontWeight.w500, + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: List.generate( + userProfile!.padSupplies!.length, (index) { + final item = userProfile.padSupplies![index]; + final isSelected = _selectedSupplyIndex == index; + return ChoiceChip( + label: + Text('${item.brand} (${item.type.label})'), + selected: isSelected, + onSelected: (selected) { + setState(() { + _selectedSupplyIndex = + selected ? index : null; + }); + }, + selectedColor: AppColors.menstrualPhase + .withValues(alpha: 0.2), + labelStyle: GoogleFonts.outfit( + color: isSelected + ? AppColors.menstrualPhase + : theme.colorScheme.onSurface, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.w400, + ), + ); + }), + ), + const SizedBox(height: 12), + ], + TextFormField( + controller: _pantylinerCountController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'Quantity Used', + hintText: '1', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12)), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + ), + onChanged: (value) { + setState(() { + _pantylinerCount = int.tryParse(value) ?? 0; + }); + }, + ), + ], + ], + ), + ), + ], + + const SizedBox(height: 16), + + // Mood + _buildSectionCard( + context, + title: 'Mood', + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: MoodLevel.values.map((mood) { + final isSelected = _mood == mood; + return GestureDetector( + onTap: () => setState(() => _mood = mood), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isSelected + ? AppColors.softGold.withValues(alpha: 0.2) + : Colors.transparent, + borderRadius: BorderRadius.circular(12), + border: isSelected + ? Border.all(color: AppColors.softGold) + : Border.all(color: Colors.transparent), + ), + child: Column( + children: [ + Text( + mood.emoji, + style: TextStyle( + fontSize: isSelected ? 32 : 28, + ), + ), + const SizedBox(height: 4), + Text( + mood.label, + style: GoogleFonts.outfit( + fontSize: 10, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.w400, + color: isSelected + ? AppColors.softGold + : theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ), + + const SizedBox(height: 16), + + // Levels + _buildSectionCard( + context, + title: 'Daily Levels', + child: Column( + children: [ + Row( + children: [ + SizedBox( + width: 80, + child: Text( + 'Energy', + style: GoogleFonts.outfit( + fontSize: 14, color: theme.colorScheme.onSurface, ), ), ), - _buildYesNoControl( - context, - value: _isSpotting, - onChanged: (value) => setState(() => _isSpotting = value), - activeColor: AppColors.menstrualPhase, + Expanded( + child: Slider( + value: (_energyLevel ?? 3).toDouble(), + min: 1, + max: 5, + divisions: 4, + activeColor: AppColors.sageGreen, + onChanged: (value) { + setState(() => _energyLevel = value.round()); + }, + ), ), - ], - ), - ), - ], - - // Still on Period? (If predicted but toggle is NO) - if (!_isPeriodDay && _shouldShowPeriodCompletionPrompt()) ...[ - const SizedBox(height: 16), - _buildSectionCard( - context, - title: 'Period Status', - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Predicted period end is near. Is your period still going, or did it finish?', - style: GoogleFonts.outfit(fontSize: 14), - ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () => setState(() => _isPeriodDay = true), - style: OutlinedButton.styleFrom( - foregroundColor: AppColors.menstrualPhase, - side: const BorderSide(color: AppColors.menstrualPhase), - ), - child: const Text('Still Going'), - ), + SizedBox( + width: 50, + child: Text( + _getEnergyLabel(_energyLevel), + textAlign: TextAlign.end, + style: GoogleFonts.outfit( + fontSize: 11, + color: theme.colorScheme.onSurfaceVariant, ), - const SizedBox(width: 12), - Expanded( - child: ElevatedButton( - onPressed: () { - // Keep _isPeriodDay as false, effectively marking as finished - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Period marked as finished.')), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.menstrualPhase, - foregroundColor: Colors.white, - ), - child: const Text('Finished'), - ), - ), - ], + ), ), ], ), - ), - ], - - // Flow Intensity (only if period day) - if (_isPeriodDay) ...[ - const SizedBox(height: 16), - _buildSectionCard( - context, - title: 'Flow Intensity', - child: Column( + const SizedBox(height: 12), + Row( children: [ - Row( - children: FlowIntensity.values.map((flow) { - final isSelected = _flowIntensity == flow; - return Expanded( - child: GestureDetector( - onTap: () => setState(() => _flowIntensity = flow), - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: isSelected - ? AppColors.menstrualPhase - .withOpacity(isDark ? 0.3 : 0.2) - : theme.colorScheme.surfaceVariant, - borderRadius: BorderRadius.circular(8), - ), - child: Center( - child: Text( - flow.toString().split('.').last, // Display enum name - style: theme.textTheme.labelLarge!.copyWith( - color: isSelected - ? Colors.white - : theme.colorScheme.onSurface, - ), - ), - ), - ), - ), - ); - }).toList(), + SizedBox( + width: 80, + child: Text( + 'Stress', + style: GoogleFonts.outfit( + fontSize: 14, + color: theme.colorScheme.onSurface, + ), + ), + ), + Expanded( + child: Slider( + value: (_stressLevel ?? 1).toDouble(), + min: 1, + max: 5, + divisions: 4, + activeColor: AppColors.ovulationPhase, + onChanged: (value) { + setState(() => _stressLevel = value.round()); + }, + ), + ), + SizedBox( + width: 50, + child: Text( + '${_stressLevel ?? 1}/5', + textAlign: TextAlign.end, + style: GoogleFonts.outfit( + fontSize: 12, + color: theme.colorScheme.onSurfaceVariant, + ), + ), ), ], ), - ), - ], + ], + ), + ), - // Supply / Material Tracking - if (isPadTrackingEnabled) ...[ - const SizedBox(height: 16), - _buildSectionCard( - context, - title: 'Supplies', - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Pad Tracker Link - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PadTrackerScreen( - isSpotting: _isSpotting, - initialFlow: _flowIntensity, - ), - ), - ); - }, - icon: const Icon(Icons.timer_outlined), - label: const Text('Pad Tracker & Reminders'), - style: OutlinedButton.styleFrom( - foregroundColor: AppColors.menstrualPhase, - side: const BorderSide(color: AppColors.menstrualPhase), - ), - ), - ), - const SizedBox(height: 16), - // Used Material Logic - Row( - children: [ - Expanded( - child: Text( - _getSupplyQuestionLabel(userProfile), - style: GoogleFonts.outfit( - fontSize: 16, - color: theme.colorScheme.onSurface, - ), - ), - ), - _buildYesNoControl( - context, - value: _usedPantyliner, - onChanged: (value) => setState(() { - _usedPantyliner = value; - if (!value) { - _pantylinerCount = 0; - _selectedSupplyIndex = null; - } - }), - activeColor: AppColors.menstrualPhase, - ), - ], - ), - - if (_usedPantyliner) ...[ - const SizedBox(height: 12), - if (userProfile?.padSupplies?.isNotEmpty == true) ...[ - Text( - 'Select item from inventory:', - style: GoogleFonts.outfit( - fontSize: 14, - fontWeight: FontWeight.w500, - color: theme.colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 8), - Wrap( - spacing: 8, - runSpacing: 8, - children: List.generate(userProfile!.padSupplies!.length, (index) { - final item = userProfile.padSupplies![index]; - final isSelected = _selectedSupplyIndex == index; - return ChoiceChip( - label: Text('${item.brand} (${item.type.label})'), - selected: isSelected, - onSelected: (selected) { - setState(() { - _selectedSupplyIndex = selected ? index : null; - }); - }, - selectedColor: AppColors.menstrualPhase.withOpacity(0.2), - labelStyle: GoogleFonts.outfit( - color: isSelected ? AppColors.menstrualPhase : theme.colorScheme.onSurface, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, - ), - ); - }), - ), - const SizedBox(height: 12), - ], - - // Count Input - TextFormField( - controller: _pantylinerCountController, - keyboardType: TextInputType.number, - decoration: InputDecoration( - labelText: 'Quantity Used', - hintText: '1', - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - ), - onChanged: (value) { - setState(() { - _pantylinerCount = int.tryParse(value) ?? 0; - }); - }, - ), - ], - ], - ), - ), - ], - - const SizedBox(height: 16), - - // Mood - _buildSectionCard( - context, - title: 'Mood', - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: MoodLevel.values.map((mood) { - final isSelected = _mood == mood; - return GestureDetector( - onTap: () => setState(() => _mood = mood), - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: isSelected - ? AppColors.softGold - .withOpacity(isDark ? 0.3 : 0.2) - : Colors.transparent, - borderRadius: BorderRadius.circular(12), - border: isSelected - ? Border.all(color: AppColors.softGold) - : Border.all(color: Colors.transparent), - ), - child: Column( - children: [ - Text( - mood.emoji, - style: TextStyle( - fontSize: isSelected ? 32 : 28, - ), - ), - const SizedBox(height: 4), - Text( - mood.label, - style: GoogleFonts.outfit( - fontSize: 10, - fontWeight: isSelected - ? FontWeight.w600 - : FontWeight.w400, - color: isSelected - ? AppColors.softGold - : theme.colorScheme.onSurfaceVariant, - ), - ), - ], + const SizedBox(height: 16), + + // Symptoms + _buildSectionCard( + context, + title: 'Symptoms', + child: Column( + children: [ + Row( + children: [ + SizedBox( + width: 80, + child: Text( + 'Cramps', + style: GoogleFonts.outfit( + fontSize: 14, + color: theme.colorScheme.onSurface, + ), ), ), - ); - }).toList(), - ), - ), - - const SizedBox(height: 16), - - // Energy & Stress Levels - _buildSectionCard( - context, - title: 'Daily Levels', - child: Column( - children: [ - // Energy Level - Row( - children: [ - SizedBox( - width: 80, - child: Text( - 'Energy', - style: GoogleFonts.outfit( - fontSize: 14, - color: theme.colorScheme.onSurface, - ), + Expanded( + child: Slider( + value: _crampIntensity.toDouble(), + min: 0, + max: 5, + divisions: 5, + activeColor: AppColors.rose, + onChanged: (value) { + setState(() => _crampIntensity = value.round()); + }, + ), + ), + SizedBox( + width: 50, + child: Text( + _crampIntensity == 0 + ? 'None' + : '$_crampIntensity/5', + textAlign: TextAlign.end, + style: GoogleFonts.outfit( + fontSize: 11, + color: theme.colorScheme.onSurfaceVariant, ), ), - Expanded( - child: Slider( - value: (_energyLevel ?? 3).toDouble(), - min: 1, - max: 5, - divisions: 4, - activeColor: AppColors.sageGreen, - onChanged: (value) { - setState(() => _energyLevel = value.round()); - }, - ), - ), - SizedBox( - width: 50, - child: Text( - _getEnergyLabel(_energyLevel), - textAlign: TextAlign.end, - style: GoogleFonts.outfit( - fontSize: 11, - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ), - ], - ), - const SizedBox(height: 12), - // Stress Level - Row( - children: [ - SizedBox( - width: 80, - child: Text( - 'Stress', - style: GoogleFonts.outfit( - fontSize: 14, - color: theme.colorScheme.onSurface, - ), - ), - ), - Expanded( - child: Slider( - value: (_stressLevel ?? 1).toDouble(), - min: 1, - max: 5, - divisions: 4, - activeColor: AppColors.ovulationPhase, - onChanged: (value) { - setState(() => _stressLevel = value.round()); - }, - ), - ), - SizedBox( - width: 50, - child: Text( - '${_stressLevel ?? 1}/5', - textAlign: TextAlign.end, - style: GoogleFonts.outfit( - fontSize: 12, - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ), - ], - ), - ], - ), - ), - - const SizedBox(height: 16), - - // Symptoms - _buildSectionCard( - context, - title: 'Symptoms', - child: Column( - children: [ - // Cramps Slider - Row( - children: [ - SizedBox( - width: 80, - child: Text( - 'Cramps', - style: GoogleFonts.outfit( - fontSize: 14, - color: theme.colorScheme.onSurface, - ), - ), - ), - Expanded( - child: Slider( - value: _crampIntensity.toDouble(), - min: 0, - max: 5, - divisions: 5, - activeColor: AppColors.rose, - onChanged: (value) { - setState(() => _crampIntensity = value.round()); - }, - ), - ), - SizedBox( - width: 50, - child: Text( - _crampIntensity == 0 - ? 'None' - : '$_crampIntensity/5', - textAlign: TextAlign.end, - style: GoogleFonts.outfit( - fontSize: 11, - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ), - ], - ), - const SizedBox(height: 12), - // Symptom Toggles - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _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)), - ], - ), - ], - ), - ), - - const SizedBox(height: 16), - - // Cravings - _buildSectionCard( - context, - title: 'Cravings', - child: TextField( - controller: _cravingsController, - decoration: InputDecoration( - hintText: 'e.g., Chocolate, salty chips (comma separated)', - filled: true, - 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), - - // Pantyliners - _buildSectionCard( - context, - title: 'Pantyliners', - child: Column( - children: [ - SwitchListTile( - title: Text('Used Pantyliner Today', style: GoogleFonts.outfit(fontSize: 14)), - value: _usedPantyliner, - onChanged: (val) => setState(() => _usedPantyliner = val), - activeColor: AppColors.menstrualPhase, - contentPadding: EdgeInsets.zero, - ), - if (_usedPantyliner) ...[ - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Count', style: GoogleFonts.outfit(fontSize: 14)), - Row( - children: [ - IconButton( - icon: const Icon(Icons.remove_circle_outline), - onPressed: _pantylinerCount > 0 - ? () => setState(() => _pantylinerCount--) - : null, - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: theme.colorScheme.surfaceVariant.withOpacity(0.3), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - '$_pantylinerCount', - style: GoogleFonts.outfit(fontWeight: FontWeight.bold), - ), - ), - IconButton( - icon: const Icon(Icons.add_circle_outline), - onPressed: () => setState(() => _pantylinerCount++), - ), - ], - ), - ], ), ], - ], + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _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)), + ], + ), + ], + ), + ), + + const SizedBox(height: 16), + + // Cravings + _buildSectionCard( + context, + title: 'Cravings', + child: TextField( + controller: _cravingsController, + decoration: InputDecoration( + hintText: 'e.g., Chocolate, salty chips (comma separated)', + filled: true, + fillColor: theme.colorScheme.surfaceContainerHighest + .withValues(alpha: 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( + ), + + const SizedBox(height: 16), + + // Intimacy + _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; + }), + activeThumbColor: 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: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: + const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: _intimacyProtected == true + ? AppColors.sageGreen + .withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( color: _intimacyProtected == true - ? AppColors.sageGreen.withOpacity(0.2) - : Colors.grey.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all( + ? AppColors.sageGreen + : Colors.grey.withValues(alpha: 0.3), + ), + ), + child: Center( + child: Text( + 'Protected', + style: GoogleFonts.outfit( + fontWeight: FontWeight.w500, 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, - ), + : AppColors.warmGray, ), ), ), ), ), - const SizedBox(width: 12), - Expanded( - child: GestureDetector( - onTap: () => setState(() => _intimacyProtected = false), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( + ), + const SizedBox(width: 12), + Expanded( + child: GestureDetector( + onTap: () => + setState(() => _intimacyProtected = false), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: + const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: _intimacyProtected == false + ? AppColors.rose.withValues(alpha: 0.15) + : Colors.grey.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( color: _intimacyProtected == false - ? AppColors.rose.withOpacity(0.15) - : Colors.grey.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all( + ? AppColors.rose + : Colors.grey.withValues(alpha: 0.3), + ), + ), + child: Center( + child: Text( + 'Unprotected', + style: GoogleFonts.outfit( + fontWeight: FontWeight.w500, 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, - ), + : 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, + ), + ], ), - ), - style: GoogleFonts.outfit( - fontSize: 14, - color: theme.colorScheme.onSurface, + ], + ], + ), + ), + + 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: theme.colorScheme.surfaceContainerHighest + .withValues(alpha: 0.1), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, ), ), - ), - - const SizedBox(height: 24), - - // Save Button - SizedBox( - width: double.infinity, - height: 54, - child: ElevatedButton( - onPressed: _saveEntry, - child: const Text('Save Entry'), + style: GoogleFonts.outfit( + fontSize: 14, + color: theme.colorScheme.onSurface, ), ), - const SizedBox(height: 40), - ], - ), - ))); + ), + + const SizedBox(height: 24), + + // Save Button + SizedBox( + width: double.infinity, + height: 54, + child: ElevatedButton( + onPressed: _saveEntry, + child: const Text('Save Entry'), + ), + ), + const SizedBox(height: 40), + ], + ), + ), + ), + ); } Widget _buildSectionCard(BuildContext context, @@ -1013,12 +991,13 @@ class _LogScreenState extends ConsumerState { decoration: BoxDecoration( color: theme.cardTheme.color, borderRadius: BorderRadius.circular(16), - border: Border.all(color: theme.colorScheme.outline.withOpacity(0.05)), + border: Border.all( + color: theme.colorScheme.outline.withValues(alpha: 0.05)), boxShadow: isDark ? null : [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -1052,7 +1031,6 @@ class _LogScreenState extends ConsumerState { return Row( mainAxisSize: MainAxisSize.min, children: [ - // No Button GestureDetector( onTap: () => onChanged(false), child: AnimatedContainer( @@ -1060,9 +1038,12 @@ class _LogScreenState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: !value - ? theme.colorScheme.error.withOpacity(isDark ? 0.3 : 0.2) - : theme.colorScheme.surfaceVariant.withOpacity(0.3), - borderRadius: const BorderRadius.horizontal(left: Radius.circular(8)), + ? theme.colorScheme.error + .withValues(alpha: isDark ? 0.3 : 0.2) + : theme.colorScheme.surfaceContainerHighest + .withValues(alpha: 0.3), + borderRadius: + const BorderRadius.horizontal(left: Radius.circular(8)), border: !value ? Border.all(color: theme.colorScheme.error) : Border.all(color: Colors.transparent), @@ -1078,7 +1059,6 @@ class _LogScreenState extends ConsumerState { ), ), ), - // Yes Button GestureDetector( onTap: () => onChanged(true), child: AnimatedContainer( @@ -1086,9 +1066,11 @@ class _LogScreenState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: value - ? activeColor.withOpacity(isDark ? 0.3 : 0.2) - : theme.colorScheme.surfaceVariant.withOpacity(0.3), - borderRadius: const BorderRadius.horizontal(right: Radius.circular(8)), + ? activeColor.withValues(alpha: isDark ? 0.3 : 0.2) + : theme.colorScheme.surfaceContainerHighest + .withValues(alpha: 0.3), + borderRadius: + const BorderRadius.horizontal(right: Radius.circular(8)), border: value ? Border.all(color: activeColor) : Border.all(color: Colors.transparent), @@ -1121,8 +1103,10 @@ class _LogScreenState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: isSelected - ? theme.colorScheme.tertiary.withOpacity(isDark ? 0.3 : 0.2) - : theme.colorScheme.surfaceVariant.withOpacity(0.3), + ? theme.colorScheme.tertiary + .withValues(alpha: isDark ? 0.3 : 0.2) + : theme.colorScheme.surfaceContainerHighest + .withValues(alpha: 0.3), borderRadius: BorderRadius.circular(20), border: isSelected ? Border.all(color: theme.colorScheme.tertiary) @@ -1195,17 +1179,20 @@ class _LogScreenState extends ConsumerState { return 'Normal'; } } + String _getSupplyQuestionLabel(UserProfile? user) { if (user == null || user.padSupplies == null || user.padSupplies!.isEmpty) { return 'Did you use any supplies today?'; } - - final hasLiners = user.padSupplies!.any((s) => s.type == PadType.pantyLiner); - // Assuming everything else is a "pad" or similar period protection + + final hasLiners = + user.padSupplies!.any((s) => s.type == PadType.pantyLiner); final hasPads = user.padSupplies!.any((s) => s.type != PadType.pantyLiner); - if (hasPads && hasLiners) return 'Did you use any supplies (pads, liners) today?'; + if (hasPads && hasLiners) { + return 'Did you use any supplies (pads, liners) today?'; + } if (hasLiners) return 'Did you use pantyliners today?'; - return 'Did you use any supplies (pads) today?'; + return 'Did you use any supplies (pads) today?'; } } diff --git a/lib/screens/log/pad_tracker_screen.dart b/lib/screens/log/pad_tracker_screen.dart index fcc74eb..91628ab 100644 --- a/lib/screens/log/pad_tracker_screen.dart +++ b/lib/screens/log/pad_tracker_screen.dart @@ -105,7 +105,7 @@ class _PadTrackerScreenState extends ConsumerState { context: context, initialTime: TimeOfDay.now(), ); - if (time != null && mounted) { + if (time != null && context.mounted) { final now = DateTime.now(); final selectedDate = DateTime( now.year, now.month, now.day, time.hour, time.minute); @@ -211,8 +211,9 @@ class _PadTrackerScreenState extends ConsumerState { SupplyItem? get _activeSupply { final user = ref.watch(userProfileProvider); - if (user == null || user.padSupplies == null || user.padSupplies!.isEmpty) + if (user == null || user.padSupplies == null || user.padSupplies!.isEmpty) { return null; + } if (_activeSupplyIndex == null || _activeSupplyIndex! >= user.padSupplies!.length) { return user.padSupplies!.first; @@ -249,7 +250,9 @@ class _PadTrackerScreenState extends ConsumerState { int get _recommendedHours { final supply = _activeSupply; - if (supply == null) return 6; // Default + if (supply == null) { + return 6; // Default + } final type = supply.type; @@ -262,9 +265,9 @@ class _PadTrackerScreenState extends ConsumerState { int baseHours; switch (_selectedFlow) { case FlowIntensity.heavy: - baseHours = (type == PadType.super_pad || + baseHours = (type == PadType.superPad || type == PadType.overnight || - type == PadType.tampon_super) + type == PadType.tamponSuper) ? 4 : 3; break; @@ -313,9 +316,7 @@ class _PadTrackerScreenState extends ConsumerState { double adjusted = baseHours * ratio; int maxHours = - (type == PadType.tampon_regular || type == PadType.tampon_super) - ? 8 - : 12; + (type == PadType.tamponRegular || type == PadType.tamponSuper) ? 8 : 12; if (adjusted < 1) adjusted = 1; if (adjusted > maxHours) adjusted = maxHours.toDouble(); @@ -364,10 +365,11 @@ class _PadTrackerScreenState extends ConsumerState { color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(16), border: Border.all( - color: AppColors.menstrualPhase.withOpacity(0.3)), + color: + AppColors.menstrualPhase.withValues(alpha: 0.3)), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -378,7 +380,8 @@ class _PadTrackerScreenState extends ConsumerState { Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: AppColors.menstrualPhase.withOpacity(0.1), + color: + AppColors.menstrualPhase.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: const Icon(Icons.inventory_2_outlined, @@ -432,7 +435,8 @@ class _PadTrackerScreenState extends ConsumerState { onSelected: (selected) { if (selected) setState(() => _selectedFlow = flow); }, - selectedColor: AppColors.menstrualPhase.withOpacity(0.3), + selectedColor: + AppColors.menstrualPhase.withValues(alpha: 0.3), labelStyle: GoogleFonts.outfit( color: _selectedFlow == flow ? AppColors.navyBlue @@ -453,13 +457,13 @@ class _PadTrackerScreenState extends ConsumerState { padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: isOverdue - ? AppColors.rose.withOpacity(0.15) - : AppColors.sageGreen.withOpacity(0.15), + ? AppColors.rose.withValues(alpha: 0.15) + : AppColors.sageGreen.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(20), border: Border.all( color: isOverdue - ? AppColors.rose.withOpacity(0.3) - : AppColors.sageGreen.withOpacity(0.3)), + ? AppColors.rose.withValues(alpha: 0.3) + : AppColors.sageGreen.withValues(alpha: 0.3)), ), child: Column( children: [ @@ -521,10 +525,10 @@ class _PadTrackerScreenState extends ConsumerState { margin: const EdgeInsets.only(bottom: 24), padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: AppColors.rose.withOpacity(0.1), + color: AppColors.rose.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), - border: - Border.all(color: AppColors.rose.withOpacity(0.3)), + border: Border.all( + color: AppColors.rose.withValues(alpha: 0.3)), ), child: Row( children: [ @@ -597,7 +601,7 @@ class _PadTrackerScreenState extends ConsumerState { _updateTimeSinceChange(); }); - if (mounted) { + if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( @@ -617,7 +621,7 @@ class _PadTrackerScreenState extends ConsumerState { backgroundColor: AppColors.menstrualPhase, foregroundColor: Colors.white, disabledBackgroundColor: - AppColors.warmGray.withOpacity(0.2), + AppColors.warmGray.withValues(alpha: 0.2), ), ), ), @@ -791,10 +795,10 @@ class _SupplyManagementPopupState margin: const EdgeInsets.only(right: 12), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: AppColors.warmCream.withOpacity(0.3), + color: AppColors.warmCream.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(16), border: Border.all( - color: AppColors.warmGray.withOpacity(0.2)), + color: AppColors.warmGray.withValues(alpha: 0.2)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -857,7 +861,7 @@ class _SupplyManagementPopupState decoration: InputDecoration( hintText: 'Brand Name (e.g. Always)', filled: true, - fillColor: AppColors.warmCream.withOpacity(0.2), + fillColor: AppColors.warmCream.withValues(alpha: 0.2), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), @@ -870,7 +874,7 @@ class _SupplyManagementPopupState children: [ Expanded( child: DropdownButtonFormField( - value: _selectedType, + initialValue: _selectedType, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 12), border: OutlineInputBorder( diff --git a/lib/screens/onboarding/onboarding_screen.dart b/lib/screens/onboarding/onboarding_screen.dart index 6b69ba6..b740fad 100644 --- a/lib/screens/onboarding/onboarding_screen.dart +++ b/lib/screens/onboarding/onboarding_screen.dart @@ -1,11 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; -import 'package:hive_flutter/hive_flutter.dart'; import 'package:uuid/uuid.dart'; import '../../theme/app_theme.dart'; import '../../models/user_profile.dart'; -import '../../models/cycle_entry.dart'; import '../home/home_screen.dart'; import '../husband/husband_home_screen.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -178,6 +175,7 @@ class _OnboardingScreenState extends ConsumerState { @override Widget build(BuildContext context) { final theme = Theme.of(context); + // final isDark = theme.brightness == Brightness.dark; - unused final isDark = theme.brightness == Brightness.dark; // Different background color for husband flow @@ -208,7 +206,7 @@ class _OnboardingScreenState extends ConsumerState { spacing: 12, activeDotColor: isHusband ? AppColors.navyBlue : AppColors.sageGreen, - dotColor: theme.colorScheme.outline.withOpacity(0.2), + dotColor: theme.colorScheme.outline.withValues(alpha: 0.2), ), ), ), @@ -255,7 +253,10 @@ class _OnboardingScreenState extends ConsumerState { height: 80, decoration: BoxDecoration( gradient: LinearGradient( - colors: [AppColors.blushPink, AppColors.rose.withOpacity(0.7)], + colors: [ + AppColors.blushPink, + AppColors.rose.withValues(alpha: 0.7) + ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), @@ -311,7 +312,7 @@ class _OnboardingScreenState extends ConsumerState { // Dynamic colors based on role selection final activeColor = role == UserRole.wife ? AppColors.sageGreen : AppColors.navyBlue; - final activeBg = activeColor.withOpacity(isDark ? 0.3 : 0.1); + final activeBg = activeColor.withValues(alpha: isDark ? 0.3 : 0.1); return GestureDetector( onTap: () => setState(() => _role = role), @@ -324,7 +325,7 @@ class _OnboardingScreenState extends ConsumerState { border: Border.all( color: isSelected ? activeColor - : theme.colorScheme.outline.withOpacity(0.1), + : theme.colorScheme.outline.withValues(alpha: 0.1), width: isSelected ? 2 : 1, ), ), @@ -333,8 +334,9 @@ class _OnboardingScreenState extends ConsumerState { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: - isSelected ? activeColor : theme.colorScheme.surfaceVariant, + color: isSelected + ? activeColor + : theme.colorScheme.surfaceContainerHighest, shape: BoxShape.circle, ), child: Icon( @@ -501,9 +503,7 @@ class _OnboardingScreenState extends ConsumerState { child: SizedBox( height: 54, child: ElevatedButton( - onPressed: (_relationshipStatus != null && !_isNavigating) - ? _nextPage - : null, + onPressed: !_isNavigating ? _nextPage : null, child: const Text('Continue'), ), ), @@ -528,13 +528,13 @@ class _OnboardingScreenState extends ConsumerState { padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isSelected - ? AppColors.sageGreen.withOpacity(isDark ? 0.3 : 0.1) + ? AppColors.sageGreen.withValues(alpha: isDark ? 0.3 : 0.1) : theme.cardTheme.color, borderRadius: BorderRadius.circular(12), border: Border.all( color: isSelected ? AppColors.sageGreen - : theme.colorScheme.outline.withOpacity(0.1), + : theme.colorScheme.outline.withValues(alpha: 0.1), width: isSelected ? 2 : 1, ), ), @@ -560,7 +560,7 @@ class _OnboardingScreenState extends ConsumerState { ), ), if (isSelected) - Icon(Icons.check_circle, color: AppColors.sageGreen), + const Icon(Icons.check_circle, color: AppColors.sageGreen), ], ), ), @@ -640,13 +640,13 @@ class _OnboardingScreenState extends ConsumerState { padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isSelected - ? AppColors.sageGreen.withOpacity(isDark ? 0.3 : 0.1) + ? AppColors.sageGreen.withValues(alpha: isDark ? 0.3 : 0.1) : theme.cardTheme.color, borderRadius: BorderRadius.circular(12), border: Border.all( color: isSelected ? AppColors.sageGreen - : theme.colorScheme.outline.withOpacity(0.1), + : theme.colorScheme.outline.withValues(alpha: 0.1), width: isSelected ? 2 : 1, ), ), @@ -672,7 +672,7 @@ class _OnboardingScreenState extends ConsumerState { ), ), if (isSelected) - Icon(Icons.check_circle, color: AppColors.sageGreen), + const Icon(Icons.check_circle, color: AppColors.sageGreen), ], ), ), @@ -681,7 +681,8 @@ class _OnboardingScreenState extends ConsumerState { Widget _buildCyclePage() { final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; + // final isDark = theme.brightness == Brightness.dark; - unused + // final isDark = theme.brightness == Brightness.dark; return Padding( padding: const EdgeInsets.all(32), @@ -812,7 +813,7 @@ class _OnboardingScreenState extends ConsumerState { color: theme.cardTheme.color, borderRadius: BorderRadius.circular(12), border: Border.all( - color: theme.colorScheme.outline.withOpacity(0.1))), + color: theme.colorScheme.outline.withValues(alpha: 0.1))), child: Row( children: [ Icon(Icons.calendar_today, @@ -878,10 +879,10 @@ class _OnboardingScreenState extends ConsumerState { width: 64, height: 64, decoration: BoxDecoration( - color: AppColors.navyBlue.withOpacity(0.1), + color: AppColors.navyBlue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16), ), - child: Icon(Icons.link, size: 32, color: AppColors.navyBlue), + child: const Icon(Icons.link, size: 32, color: AppColors.navyBlue), ), const SizedBox(height: 24), Text( @@ -975,10 +976,11 @@ class _OnboardingScreenState extends ConsumerState { width: 64, height: 64, decoration: BoxDecoration( - color: AppColors.sageGreen.withOpacity(0.1), + color: AppColors.sageGreen.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16), ), - child: Icon(Icons.favorite, size: 32, color: AppColors.sageGreen), + child: const Icon(Icons.favorite, + size: 32, color: AppColors.sageGreen), ), const SizedBox(height: 24), Text( @@ -1073,12 +1075,13 @@ class _OnboardingScreenState extends ConsumerState { padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isSelected - ? color.withOpacity(isDark ? 0.3 : 0.1) + ? color.withValues(alpha: isDark ? 0.3 : 0.1) : theme.cardTheme.color, borderRadius: BorderRadius.circular(12), border: Border.all( - color: - isSelected ? color : theme.colorScheme.outline.withOpacity(0.1), + color: isSelected + ? color + : theme.colorScheme.outline.withValues(alpha: 0.1), width: isSelected ? 2 : 1, ), ), @@ -1087,7 +1090,9 @@ class _OnboardingScreenState extends ConsumerState { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: isSelected ? color : theme.colorScheme.surfaceVariant, + color: isSelected + ? color + : theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: Icon( diff --git a/lib/screens/settings/appearance_screen.dart b/lib/screens/settings/appearance_screen.dart index 6d22574..ba04208 100644 --- a/lib/screens/settings/appearance_screen.dart +++ b/lib/screens/settings/appearance_screen.dart @@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/user_profile.dart'; import '../../providers/user_provider.dart'; import '../../theme/app_theme.dart'; -import '../../widgets/pad_settings_dialog.dart'; class AppearanceScreen extends ConsumerWidget { const AppearanceScreen({super.key}); @@ -43,41 +42,42 @@ class AppearanceScreen extends ConsumerWidget { ), const SizedBox(height: 16), SegmentedButton( - segments: const [ - ButtonSegment( - value: AppThemeMode.light, - label: Text('Light'), - icon: Icon(Icons.light_mode), - ), - ButtonSegment( - value: AppThemeMode.dark, - label: Text('Dark'), - icon: Icon(Icons.dark_mode), - ), - ButtonSegment( - value: AppThemeMode.system, - label: Text('System'), - icon: Icon(Icons.brightness_auto), - ), - ], - selected: {currentMode}, - onSelectionChanged: (Set newSelection) { - if (newSelection.isNotEmpty) { - ref - .read(userProfileProvider.notifier) - .updateThemeMode(newSelection.first); - } - }, - style: SegmentedButton.styleFrom( - fixedSize: const Size.fromHeight(48), - ) - ), + segments: const [ + ButtonSegment( + value: AppThemeMode.light, + label: Text('Light'), + icon: Icon(Icons.light_mode), + ), + ButtonSegment( + value: AppThemeMode.dark, + label: Text('Dark'), + icon: Icon(Icons.dark_mode), + ), + ButtonSegment( + value: AppThemeMode.system, + label: Text('System'), + icon: Icon(Icons.brightness_auto), + ), + ], + selected: { + currentMode + }, + onSelectionChanged: (Set newSelection) { + if (newSelection.isNotEmpty) { + ref + .read(userProfileProvider.notifier) + .updateThemeMode(newSelection.first); + } + }, + style: SegmentedButton.styleFrom( + fixedSize: const Size.fromHeight(48), + )), ], ); } - Widget _buildAccentColorSelector(BuildContext context, WidgetRef ref, - String currentAccent) { + Widget _buildAccentColorSelector( + BuildContext context, WidgetRef ref, String currentAccent) { final accents = [ {'color': AppColors.sageGreen, 'value': '0xFFA8C5A8'}, {'color': AppColors.rose, 'value': '0xFFE8A0B0'}, @@ -125,7 +125,7 @@ class AppearanceScreen extends ConsumerWidget { boxShadow: [ if (isSelected) BoxShadow( - color: color.withOpacity(0.4), + color: color.withValues(alpha: 0.4), blurRadius: 8, offset: const Offset(0, 4), ) @@ -141,4 +141,4 @@ class AppearanceScreen extends ConsumerWidget { ], ); } -} \ No newline at end of file +} diff --git a/lib/screens/settings/cycle_settings_screen.dart b/lib/screens/settings/cycle_settings_screen.dart index 987365b..6eed83c 100644 --- a/lib/screens/settings/cycle_settings_screen.dart +++ b/lib/screens/settings/cycle_settings_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; -import '../../models/user_profile.dart'; import '../../providers/user_provider.dart'; class CycleSettingsScreen extends ConsumerStatefulWidget { @@ -45,8 +44,10 @@ class _CycleSettingsScreenState extends ConsumerState { final userProfile = ref.read(userProfileProvider); if (userProfile != null) { final updatedProfile = userProfile.copyWith( - averageCycleLength: int.tryParse(_cycleLengthController.text) ?? userProfile.averageCycleLength, - averagePeriodLength: int.tryParse(_periodLengthController.text) ?? userProfile.averagePeriodLength, + averageCycleLength: int.tryParse(_cycleLengthController.text) ?? + userProfile.averageCycleLength, + averagePeriodLength: int.tryParse(_periodLengthController.text) ?? + userProfile.averagePeriodLength, lastPeriodStartDate: _lastPeriodStartDate, isIrregularCycle: _isIrregularCycle, isPadTrackingEnabled: _isPadTrackingEnabled, diff --git a/lib/screens/settings/export_data_screen.dart b/lib/screens/settings/export_data_screen.dart index 143a502..1c552a7 100644 --- a/lib/screens/settings/export_data_screen.dart +++ b/lib/screens/settings/export_data_screen.dart @@ -24,28 +24,40 @@ class ExportDataScreen extends ConsumerWidget { ListTile( leading: const Icon(Icons.picture_as_pdf), title: const Text('Export as PDF'), - subtitle: const Text('Generate a printable PDF report of your cycle data.'), + subtitle: const Text( + 'Generate a printable PDF report of your cycle data.'), trailing: const Icon(Icons.chevron_right), onTap: () async { try { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Generating PDF report...')), - ); - await PdfService.generateCycleReport(userProfile, cycleEntries); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('PDF report generated successfully!')), - ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Generating PDF report...')), + ); + } + await PdfService.generateCycleReport( + userProfile, cycleEntries); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('PDF report generated successfully!')), + ); + } } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to generate PDF: $e')), - ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to generate PDF: $e')), + ); + } } }, ), ListTile( leading: const Icon(Icons.sync), title: const Text('Sync with Calendar'), - subtitle: const Text('Export to Apple, Google, or Outlook Calendar.'), + subtitle: const Text( + 'Export to Apple, Google, or Outlook Calendar.'), trailing: const Icon(Icons.chevron_right), onTap: () async { // Show options dialog @@ -53,7 +65,8 @@ class ExportDataScreen extends ConsumerWidget { context: context, builder: (context) => AlertDialog( title: const Text('Calendar Sync Options'), - content: const Text('Would you like to include predicted future periods for the next 12 months?'), + content: const Text( + 'Would you like to include predicted future periods for the next 12 months?'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), @@ -67,26 +80,37 @@ class ExportDataScreen extends ConsumerWidget { ), ); - if (includePredictions == null) return; // User cancelled dialog (though I didn't add cancel button, tapping outside returns null) + if (includePredictions == null) { + return; // User cancelled dialog + } try { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Generating calendar file...')), - ); - - await IcsService.generateCycleCalendar( - cycleEntries, - user: userProfile, - includePredictions: includePredictions - ); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Calendar file generated! Open it to add to your calendar.')), - ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Generating calendar file...')), + ); + } + + await IcsService.generateCycleCalendar(cycleEntries, + user: userProfile, + includePredictions: includePredictions); + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Calendar file generated! Open it to add to your calendar.')), + ); + } } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to generate calendar file: $e')), - ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Failed to generate calendar file: $e')), + ); + } } }, ), diff --git a/lib/screens/settings/goal_settings_screen.dart b/lib/screens/settings/goal_settings_screen.dart index 62e5c23..730fba3 100644 --- a/lib/screens/settings/goal_settings_screen.dart +++ b/lib/screens/settings/goal_settings_screen.dart @@ -18,47 +18,60 @@ class GoalSettingsScreen extends ConsumerWidget { appBar: AppBar( title: const Text('Cycle Goal'), ), - body: ListView( - padding: const EdgeInsets.all(16.0), - children: [ - const Text( - 'What is your current goal?', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - const Text( - 'Select your primary goal to get personalized insights and predictions.', - style: TextStyle(fontSize: 14, color: Colors.grey), - ), - const SizedBox(height: 24), - _buildGoalOption( - context, - ref, - title: 'Track Cycle Only', - subtitle: 'Monitor period and health without fertility focus', - value: FertilityGoal.justTracking, - groupValue: userProfile.fertilityGoal, - icon: Icons.calendar_today, - ), - _buildGoalOption( - context, - ref, - title: 'Achieve Pregnancy', - subtitle: 'Identify fertile window and ovulation', - value: FertilityGoal.tryingToConceive, - groupValue: userProfile.fertilityGoal, - icon: Icons.child_friendly, - ), - _buildGoalOption( - context, - ref, - title: 'Avoid Pregnancy', - subtitle: 'Track fertility for natural family planning', - value: FertilityGoal.tryingToAvoid, - groupValue: userProfile.fertilityGoal, - icon: Icons.security, - ), - ], + body: RadioGroup( + groupValue: userProfile.fertilityGoal, + onChanged: (FertilityGoal? newValue) { + if (newValue != null) { + final currentProfile = ref.read(userProfileProvider); + if (currentProfile != null) { + ref.read(userProfileProvider.notifier).updateProfile( + currentProfile.copyWith(fertilityGoal: newValue), + ); + } + } + }, + child: ListView( + padding: const EdgeInsets.all(16.0), + children: [ + const Text( + 'What is your current goal?', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text( + 'Select your primary goal to get personalized insights and predictions.', + style: TextStyle(fontSize: 14, color: Colors.grey), + ), + const SizedBox(height: 24), + _buildGoalOption( + context, + ref, + title: 'Track Cycle Only', + subtitle: 'Monitor period and health without fertility focus', + value: FertilityGoal.justTracking, + groupValue: userProfile.fertilityGoal, + icon: Icons.calendar_today, + ), + _buildGoalOption( + context, + ref, + title: 'Achieve Pregnancy', + subtitle: 'Identify fertile window and ovulation', + value: FertilityGoal.tryingToConceive, + groupValue: userProfile.fertilityGoal, + icon: Icons.child_friendly, + ), + _buildGoalOption( + context, + ref, + title: 'Avoid Pregnancy', + subtitle: 'Track fertility for natural family planning', + value: FertilityGoal.tryingToAvoid, + groupValue: userProfile.fertilityGoal, + icon: Icons.security, + ), + ], + ), ), ); } @@ -85,20 +98,10 @@ class GoalSettingsScreen extends ConsumerWidget { ), child: RadioListTile( value: value, - groupValue: groupValue, - onChanged: (FertilityGoal? newValue) { - if (newValue != null) { - final currentProfile = ref.read(userProfileProvider); - if (currentProfile != null) { - ref.read(userProfileProvider.notifier).updateProfile( - currentProfile.copyWith(fertilityGoal: newValue), - ); - } - } - }, title: Text(title, style: const TextStyle(fontWeight: FontWeight.w600)), subtitle: Text(subtitle), - secondary: Icon(icon, color: isSelected ? Theme.of(context).colorScheme.primary : null), + secondary: Icon(icon, + color: isSelected ? Theme.of(context).colorScheme.primary : null), activeColor: Theme.of(context).colorScheme.primary, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), diff --git a/lib/screens/settings/notification_settings_screen.dart b/lib/screens/settings/notification_settings_screen.dart index 6574e52..8ba2d64 100644 --- a/lib/screens/settings/notification_settings_screen.dart +++ b/lib/screens/settings/notification_settings_screen.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../models/user_profile.dart'; // Import UserProfile import '../../providers/user_provider.dart'; class NotificationSettingsScreen extends ConsumerWidget { @@ -26,29 +25,30 @@ class NotificationSettingsScreen extends ConsumerWidget { children: [ SwitchListTile( title: const Text('Period Estimate'), - subtitle: const Text('Get notified when your period is predicted to start soon.'), + subtitle: const Text( + 'Get notified when your period is predicted to start soon.'), value: userProfile.notifyPeriodEstimate, onChanged: (value) async { - await ref - .read(userProfileProvider.notifier) - .updateProfile(userProfile.copyWith(notifyPeriodEstimate: value)); + await ref.read(userProfileProvider.notifier).updateProfile( + userProfile.copyWith(notifyPeriodEstimate: value)); }, ), const Divider(), SwitchListTile( title: const Text('Period Start'), - subtitle: const Text('Get notified when a period starts (or husband needs to know).'), + subtitle: const Text( + 'Get notified when a period starts (or husband needs to know).'), value: userProfile.notifyPeriodStart, onChanged: (value) async { - await ref - .read(userProfileProvider.notifier) - .updateProfile(userProfile.copyWith(notifyPeriodStart: value)); + await ref.read(userProfileProvider.notifier).updateProfile( + userProfile.copyWith(notifyPeriodStart: value)); }, ), const Divider(), SwitchListTile( title: const Text('Low Supply Alert'), - subtitle: const Text('Get notified when pad inventory is running low.'), + subtitle: + const Text('Get notified when pad inventory is running low.'), value: userProfile.notifyLowSupply, onChanged: (value) async { await ref @@ -58,14 +58,15 @@ class NotificationSettingsScreen extends ConsumerWidget { ), if (userProfile.isPadTrackingEnabled) ...[ const Divider(), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + Padding( + padding: + const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), child: Text( 'Pad Change Reminders', style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - ), + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), ), ), CheckboxListTile( @@ -100,12 +101,14 @@ class NotificationSettingsScreen extends ConsumerWidget { ), CheckboxListTile( title: const Text('Change Now (Time\'s Up)'), - subtitle: const Text('Get notified when it\'s recommended to change.'), + subtitle: + const Text('Get notified when it\'s recommended to change.'), value: userProfile.notifyPadNow, onChanged: (value) async { if (value != null) { - await ref.read(userProfileProvider.notifier).updateProfile( - userProfile.copyWith(notifyPadNow: value)); + await ref + .read(userProfileProvider.notifier) + .updateProfile(userProfile.copyWith(notifyPadNow: value)); } }, ), diff --git a/lib/screens/settings/privacy_settings_screen.dart b/lib/screens/settings/privacy_settings_screen.dart index ca3fe94..91b990d 100644 --- a/lib/screens/settings/privacy_settings_screen.dart +++ b/lib/screens/settings/privacy_settings_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:health/health.dart'; + import '../../providers/user_provider.dart'; import '../../services/health_service.dart'; @@ -23,24 +23,30 @@ class _PrivacySettingsScreenState extends ConsumerState { } Future _checkPermissions() async { - final hasPermissions = await _healthService.hasPermissions(_healthService.menstruationDataTypes); + final hasPermissions = await _healthService + .hasPermissions(_healthService.menstruationDataTypes); setState(() { _hasPermissions = hasPermissions; }); } Future _requestPermissions() async { - final authorized = await _healthService.requestAuthorization(_healthService.menstruationDataTypes); + final authorized = await _healthService + .requestAuthorization(_healthService.menstruationDataTypes); if (authorized) { _hasPermissions = true; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Health app access granted!')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Health app access granted!')), + ); + } } else { _hasPermissions = false; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Health app access denied.')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Health app access denied.')), + ); + } } setState(() {}); // Rebuild to update UI } @@ -48,14 +54,17 @@ class _PrivacySettingsScreenState extends ConsumerState { Future _syncPeriodDays(bool sync) async { if (sync) { if (!_hasPermissions) { - // Request permissions if not already granted - final authorized = await _healthService.requestAuthorization(_healthService.menstruationDataTypes); - if (!authorized) { - if (mounted) { + final authorized = await _healthService + .requestAuthorization(_healthService.menstruationDataTypes); + if (mounted) { + if (!authorized) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Cannot sync without health app permissions.')), + const SnackBar( + content: Text('Cannot sync without health app permissions.')), ); + return; } + } else { return; } _hasPermissions = true; @@ -70,7 +79,8 @@ class _PrivacySettingsScreenState extends ConsumerState { final cycleEntries = ref.read(cycleEntriesProvider); if (userProfile != null) { - final success = await _healthService.writeMenstruationData(cycleEntries); + final success = + await _healthService.writeMenstruationData(cycleEntries); if (mounted) { if (success) { ScaffoldMessenger.of(context).showSnackBar( @@ -90,7 +100,7 @@ class _PrivacySettingsScreenState extends ConsumerState { ); } } - setState(() {}); // Rebuild to update UI + setState(() {}); } Future _setPin() async { @@ -98,38 +108,40 @@ class _PrivacySettingsScreenState extends ConsumerState { if (pin != null && pin.length >= 4) { final user = ref.read(userProfileProvider); if (user != null) { - await ref.read(userProfileProvider.notifier).updateProfile(user.copyWith(privacyPin: pin)); - if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('PIN Set Successfully'))); + await ref + .read(userProfileProvider.notifier) + .updateProfile(user.copyWith(privacyPin: pin)); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('PIN Set Successfully'))); + } } } } - + Future _removePin() async { final user = ref.read(userProfileProvider); if (user == null) return; - - // Require current PIN - final currentPin = await _showPinDialog(context, title: 'Enter Current PIN'); + + final currentPin = + await _showPinDialog(context, title: 'Enter Current PIN'); if (currentPin == user.privacyPin) { - await ref.read(userProfileProvider.notifier).updateProfile( - // To clear fields, copyWith might need to handle nulls explicitly if written that way, - // but here we might pass empty string or handle logic. - // Actually, copyWith signature usually ignores nulls. - // I'll assume updating with empty string or handle it in provider, - // but for now let's just use empty string to signify removal if logic supports it. - // Wait, copyWith `privacyPin: privacyPin ?? this.privacyPin`. - // If I pass null, it keeps existing. I can't clear it via standard copyWith unless I change copyWith logic or pass emptiness. - // I'll update the userProfile object directly and save? No, Hive object. - // For now, let's treat "empty string" as no PIN if I can pass it. - user.copyWith(privacyPin: '', isBioProtected: false, isHistoryProtected: false) - ); - if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('PIN Removed'))); + await ref.read(userProfileProvider.notifier).updateProfile(user.copyWith( + privacyPin: '', isBioProtected: false, isHistoryProtected: false)); + if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('PIN Removed'))); + } } else { - if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Incorrect PIN'))); + if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('Incorrect PIN'))); + } } } - Future _showPinDialog(BuildContext context, {required String title}) { + Future _showPinDialog(BuildContext context, + {required String title}) { final controller = TextEditingController(); return showDialog( context: context, @@ -143,7 +155,9 @@ class _PrivacySettingsScreenState extends ConsumerState { decoration: const InputDecoration(hintText: 'Enter 4-digit PIN'), ), actions: [ - TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel')), ElevatedButton( onPressed: () => Navigator.pop(context, controller.text), child: const Text('OK'), @@ -155,9 +169,13 @@ class _PrivacySettingsScreenState extends ConsumerState { @override Widget build(BuildContext context) { - bool syncPeriodToHealth = _hasPermissions; final user = ref.watch(userProfileProvider); - final hasPin = user?.privacyPin != null && user!.privacyPin!.isNotEmpty; + if (user == null) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + + final hasPin = user.privacyPin != null && user.privacyPin!.isNotEmpty; + bool syncPeriodToHealth = _hasPermissions; return Scaffold( appBar: AppBar( @@ -166,111 +184,135 @@ class _PrivacySettingsScreenState extends ConsumerState { body: ListView( padding: const EdgeInsets.all(16.0), children: [ - // Security Section - Text('App Security', style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Theme.of(context).colorScheme.primary)), + Text('App Security', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(color: Theme.of(context).colorScheme.primary)), const SizedBox(height: 8), ListTile( title: const Text('Privacy PIN'), - subtitle: Text(hasPin ? 'PIN is set' : 'Protect sensitive data with a PIN'), - trailing: hasPin ? const Icon(Icons.lock, color: Colors.green) : const Icon(Icons.lock_open), + subtitle: Text( + hasPin ? 'PIN is set' : 'Protect sensitive data with a PIN'), + trailing: hasPin + ? const Icon(Icons.lock, color: Colors.green) + : const Icon(Icons.lock_open), onTap: () { if (hasPin) { - showModalBottomSheet(context: context, builder: (context) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.edit), - title: const Text('Change PIN'), - onTap: () { - Navigator.pop(context); - _setPin(); - }, - ), - ListTile( - leading: const Icon(Icons.delete, color: Colors.red), - title: const Text('Remove PIN'), - onTap: () { - Navigator.pop(context); - _removePin(); - }, - ), - ], - )); + showModalBottomSheet( + context: context, + builder: (context) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.edit), + title: const Text('Change PIN'), + onTap: () { + Navigator.pop(context); + _setPin(); + }, + ), + ListTile( + leading: + const Icon(Icons.delete, color: Colors.red), + title: const Text('Remove PIN'), + onTap: () { + Navigator.pop(context); + _removePin(); + }, + ), + ], + )); } else { _setPin(); } }, ), - if (hasPin) ...[ SwitchListTile( title: const Text('Use Biometrics'), subtitle: const Text('Unlock with FaceID / Fingerprint'), - value: user?.isBioProtected ?? false, + value: user.isBioProtected, onChanged: (val) { - ref.read(userProfileProvider.notifier).updateProfile(user!.copyWith(isBioProtected: val)); + ref + .read(userProfileProvider.notifier) + .updateProfile(user.copyWith(isBioProtected: val)); }, ), - const Divider(), - const Text('Protected Features', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey)), + const Text('Protected Features', + style: + TextStyle(fontWeight: FontWeight.bold, color: Colors.grey)), SwitchListTile( title: const Text('Daily Logs'), - value: user?.isLogProtected ?? false, + value: user.isLogProtected, onChanged: (val) { - ref.read(userProfileProvider.notifier).updateProfile(user!.copyWith(isLogProtected: val)); + ref + .read(userProfileProvider.notifier) + .updateProfile(user.copyWith(isLogProtected: val)); }, ), - SwitchListTile( + SwitchListTile( title: const Text('Calendar'), - value: user?.isCalendarProtected ?? false, + value: user.isCalendarProtected, onChanged: (val) { - ref.read(userProfileProvider.notifier).updateProfile(user!.copyWith(isCalendarProtected: val)); + ref + .read(userProfileProvider.notifier) + .updateProfile(user.copyWith(isCalendarProtected: val)); }, ), - SwitchListTile( + SwitchListTile( title: const Text('Supplies / Pad Tracker'), - value: user?.isSuppliesProtected ?? false, + value: user.isSuppliesProtected, onChanged: (val) { - ref.read(userProfileProvider.notifier).updateProfile(user!.copyWith(isSuppliesProtected: val)); + ref + .read(userProfileProvider.notifier) + .updateProfile(user.copyWith(isSuppliesProtected: val)); }, ), SwitchListTile( title: const Text('Cycle History'), - value: user?.isHistoryProtected ?? false, + value: user.isHistoryProtected, onChanged: (val) { - ref.read(userProfileProvider.notifier).updateProfile(user!.copyWith(isHistoryProtected: val)); + ref + .read(userProfileProvider.notifier) + .updateProfile(user.copyWith(isHistoryProtected: val)); }, ), ], - const Divider(height: 32), - - // Health Section - Text('Health App Integration', style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Theme.of(context).colorScheme.primary)), + Text('Health App Integration', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(color: Theme.of(context).colorScheme.primary)), const SizedBox(height: 8), ListTile( title: const Text('Health Source'), subtitle: _hasPermissions ? const Text('Connected to Health App.') : const Text('Not connected. Tap to grant access.'), - trailing: _hasPermissions ? const Icon(Icons.check_circle, color: Colors.green) : const Icon(Icons.warning, color: Colors.orange), + trailing: _hasPermissions + ? const Icon(Icons.check_circle, color: Colors.green) + : const Icon(Icons.warning, color: Colors.orange), onTap: _requestPermissions, ), SwitchListTile( title: const Text('Sync Period Days'), subtitle: const Text('Automatically sync period dates.'), value: syncPeriodToHealth, - onChanged: _hasPermissions ? (value) async { - if (value) { - await _syncPeriodDays(true); - } else { - await _syncPeriodDays(false); - } - setState(() { - syncPeriodToHealth = value; // Update local state for toggle - }); - } : null, + onChanged: _hasPermissions + ? (value) async { + if (value) { + await _syncPeriodDays(true); + } else { + await _syncPeriodDays(false); + } + setState(() { + syncPeriodToHealth = value; + }); + } + : null, ), ], ), diff --git a/lib/screens/settings/relationship_settings_screen.dart b/lib/screens/settings/relationship_settings_screen.dart index 7e60df1..5d8b0e0 100644 --- a/lib/screens/settings/relationship_settings_screen.dart +++ b/lib/screens/settings/relationship_settings_screen.dart @@ -17,70 +17,85 @@ class RelationshipSettingsScreen extends ConsumerWidget { ), body: userProfile == null ? const Center(child: CircularProgressIndicator()) - : ListView( - padding: const EdgeInsets.all(16.0), - children: [ - const Text( - 'Select your current relationship status to customize your experience.', - style: TextStyle(fontSize: 16), - ), - const SizedBox(height: 16), - // Sample Data Button - Center( - child: TextButton.icon( - onPressed: () { - final user = ref.read(userProfileProvider); - if (user != null) { - final samplePlan = TeachingPlan.create( - topic: 'Walking in Love', - scriptureReference: 'Ephesians 5:1-2', - notes: 'As Christ loved us and gave himself up for us, a fragrant offering and sacrifice to God. Let our marriage reflect this sacrificial love.', - date: DateTime.now(), - ); - - final List updatedPlans = [...(user.teachingPlans ?? []), samplePlan]; - ref.read(userProfileProvider.notifier).updateProfile( - user.copyWith(teachingPlans: updatedPlans) - ); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Sample Teaching Plan Loaded! Check Devotional page.')), - ); - } - }, - icon: const Icon(Icons.science_outlined), - label: const Text('Load Sample Teaching Plan (Demo)'), + : RadioGroup( + groupValue: userProfile.relationshipStatus, + onChanged: (RelationshipStatus? newValue) { + if (newValue != null) { + ref + .read(userProfileProvider.notifier) + .updateRelationshipStatus(newValue); + } + }, + child: ListView( + padding: const EdgeInsets.all(16.0), + children: [ + const Text( + 'Select your current relationship status to customize your experience.', + style: TextStyle(fontSize: 16), ), - ), - const SizedBox(height: 24), - _buildOption( - context, - ref, - title: 'Single', - subtitle: 'Tracking for potential future', - value: RelationshipStatus.single, - groupValue: userProfile.relationshipStatus, - icon: Icons.person_outline, - ), - _buildOption( - context, - ref, - title: 'Engaged', - subtitle: 'Preparing for marriage', - value: RelationshipStatus.engaged, - groupValue: userProfile.relationshipStatus, - icon: Icons.favorite_border, - ), - _buildOption( - context, - ref, - title: 'Married', - subtitle: 'Tracking together with husband', - value: RelationshipStatus.married, - groupValue: userProfile.relationshipStatus, - icon: Icons.favorite, - ), - ], + const SizedBox(height: 16), + // Sample Data Button + Center( + child: TextButton.icon( + onPressed: () { + final user = ref.read(userProfileProvider); + if (user != null) { + final samplePlan = TeachingPlan.create( + topic: 'Walking in Love', + scriptureReference: 'Ephesians 5:1-2', + notes: + 'As Christ loved us and gave himself up for us, a fragrant offering and sacrifice to God. Let our marriage reflect this sacrificial love.', + date: DateTime.now(), + ); + + final List updatedPlans = [ + ...(user.teachingPlans ?? []), + samplePlan + ]; + ref.read(userProfileProvider.notifier).updateProfile( + user.copyWith(teachingPlans: updatedPlans)); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Sample Teaching Plan Loaded! Check Devotional page.')), + ); + } + }, + icon: const Icon(Icons.science_outlined), + label: const Text('Load Sample Teaching Plan (Demo)'), + ), + ), + const SizedBox(height: 24), + _buildOption( + context, + ref, + title: 'Single', + subtitle: 'Tracking for potential future', + value: RelationshipStatus.single, + groupValue: userProfile.relationshipStatus, + icon: Icons.person_outline, + ), + _buildOption( + context, + ref, + title: 'Engaged', + subtitle: 'Preparing for marriage', + value: RelationshipStatus.engaged, + groupValue: userProfile.relationshipStatus, + icon: Icons.favorite_border, + ), + _buildOption( + context, + ref, + title: 'Married', + subtitle: 'Tracking together with husband', + value: RelationshipStatus.married, + groupValue: userProfile.relationshipStatus, + icon: Icons.favorite, + ), + ], + ), ), ); } @@ -98,7 +113,7 @@ class RelationshipSettingsScreen extends ConsumerWidget { return Card( elevation: isSelected ? 2 : 0, - shape: RoundedRectangleBorder( + shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: isSelected ? BorderSide(color: Theme.of(context).colorScheme.primary, width: 2) @@ -106,17 +121,10 @@ class RelationshipSettingsScreen extends ConsumerWidget { ), child: RadioListTile( value: value, - groupValue: groupValue, - onChanged: (RelationshipStatus? newValue) { - if (newValue != null) { - ref - .read(userProfileProvider.notifier) - .updateRelationshipStatus(newValue); - } - }, title: Text(title, style: const TextStyle(fontWeight: FontWeight.w600)), subtitle: Text(subtitle), - secondary: Icon(icon, color: isSelected ? Theme.of(context).colorScheme.primary : null), + secondary: Icon(icon, + color: isSelected ? Theme.of(context).colorScheme.primary : null), activeColor: Theme.of(context).colorScheme.primary, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), diff --git a/lib/screens/settings/sharing_settings_screen.dart b/lib/screens/settings/sharing_settings_screen.dart index d6657b5..313dc7e 100644 --- a/lib/screens/settings/sharing_settings_screen.dart +++ b/lib/screens/settings/sharing_settings_screen.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../theme/app_theme.dart'; -import '../../models/user_profile.dart'; import '../../providers/user_provider.dart'; class SharingSettingsScreen extends ConsumerWidget { @@ -31,7 +30,9 @@ class SharingSettingsScreen extends ConsumerWidget { ListTile( leading: const Icon(Icons.link), title: const Text('Link with Husband'), - subtitle: Text(userProfile.partnerName != null ? 'Linked to ${userProfile.partnerName}' : 'Not linked'), + subtitle: Text(userProfile.partnerName != null + ? 'Linked to ${userProfile.partnerName}' + : 'Not linked'), trailing: const Icon(Icons.chevron_right), onTap: () => _showShareDialog(context, ref), ), @@ -67,9 +68,8 @@ class SharingSettingsScreen extends ConsumerWidget { title: const Text('Share Energy Levels'), value: userProfile.shareEnergyLevels, onChanged: (value) { - ref - .read(userProfileProvider.notifier) - .updateProfile(userProfile.copyWith(shareEnergyLevels: value)); + ref.read(userProfileProvider.notifier).updateProfile( + userProfile.copyWith(shareEnergyLevels: value)); }, ), SwitchListTile( @@ -98,16 +98,17 @@ class SharingSettingsScreen extends ConsumerWidget { void _showShareDialog(BuildContext context, WidgetRef ref) { // Generate a simple pairing code final userProfile = ref.read(userProfileProvider); - final pairingCode = userProfile?.id?.substring(0, 6).toUpperCase() ?? 'ABC123'; + final pairingCode = + userProfile?.id.substring(0, 6).toUpperCase() ?? 'ABC123'; showDialog( context: context, builder: (context) => AlertDialog( - title: Row( + title: const Row( children: [ Icon(Icons.share_outlined, color: AppColors.navyBlue), - const SizedBox(width: 8), - const Text('Share with Husband'), + SizedBox(width: 8), + Text('Share with Husband'), ], ), content: Column( @@ -115,15 +116,17 @@ class SharingSettingsScreen extends ConsumerWidget { children: [ Text( 'Share this code with your husband so he can connect to your cycle data:', - style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray), + 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.navyBlue.withOpacity(0.1), + color: AppColors.navyBlue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.navyBlue.withOpacity(0.3)), + border: Border.all( + color: AppColors.navyBlue.withValues(alpha: 0.3)), ), child: SelectableText( pairingCode, @@ -138,7 +141,8 @@ class SharingSettingsScreen extends ConsumerWidget { 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), + style: + GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), textAlign: TextAlign.center, ), ], diff --git a/lib/screens/settings/supplies_settings_screen.dart b/lib/screens/settings/supplies_settings_screen.dart index 3933d4b..5a45426 100644 --- a/lib/screens/settings/supplies_settings_screen.dart +++ b/lib/screens/settings/supplies_settings_screen.dart @@ -5,10 +5,9 @@ import '../../theme/app_theme.dart'; import '../../models/user_profile.dart'; import '../../providers/user_provider.dart'; import '../../services/notification_service.dart'; -import '../../widgets/pad_settings_dialog.dart'; // We can reuse the logic, but maybe embed it directly or just link it. // Actually, let's rebuild the UI here properly as a screen instead of a dialog, -// or for now, since we already have the dialog logic working well, let's just -// have this screen trigger the dialog or embed the same widgets. +// or for now, since we already have the dialog logic working well, let's just +// have this screen trigger the dialog or embed the same widgets. // However, the user asked to "make a new setting page", so a full screen is better. // I'll copy the logic from the dialog into this screen for a seamless experience. @@ -16,16 +15,18 @@ class SuppliesSettingsScreen extends ConsumerStatefulWidget { const SuppliesSettingsScreen({super.key}); @override - ConsumerState createState() => _SuppliesSettingsScreenState(); + ConsumerState createState() => + _SuppliesSettingsScreenState(); } -class _SuppliesSettingsScreenState extends ConsumerState { +class _SuppliesSettingsScreenState + extends ConsumerState { bool _isTrackingEnabled = false; - int _typicalFlow = 2; + int _typicalFlow = 2; bool _isAutoInventoryEnabled = true; bool _showPadTimerMinutes = true; bool _showPadTimerSeconds = false; - + // Inventory List _supplies = []; int _lowInventoryThreshold = 5; @@ -41,7 +42,7 @@ class _SuppliesSettingsScreenState extends ConsumerState _lowInventoryThreshold = user.lowInventoryThreshold; _showPadTimerMinutes = user.showPadTimerMinutes; _showPadTimerSeconds = user.showPadTimerSeconds; - + // Load supplies if (user.padSupplies != null) { _supplies = List.from(user.padSupplies!); @@ -65,19 +66,21 @@ class _SuppliesSettingsScreenState extends ConsumerState padInventoryCount: totalCount, lowInventoryThreshold: _lowInventoryThreshold, ); - - await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); + + await ref + .read(userProfileProvider.notifier) + .updateProfile(updatedProfile); // Check for Low Supply Alert - if (updatedProfile.notifyLowSupply && + if (updatedProfile.notifyLowSupply && totalCount <= updatedProfile.lowInventoryThreshold) { - NotificationService().showLocalNotification( - id: 2001, - title: 'Low Pad Supply', - body: 'Your inventory is low ($totalCount left). Time to restock!', - ); + NotificationService().showLocalNotification( + id: 2001, + title: 'Low Pad Supply', + body: 'Your inventory is low ($totalCount left). Time to restock!', + ); } - + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Preferences saved')), @@ -121,7 +124,7 @@ class _SuppliesSettingsScreenState extends ConsumerState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Toggle + // Toggle Row( children: [ Expanded( @@ -137,14 +140,14 @@ class _SuppliesSettingsScreenState extends ConsumerState Switch( value: _isTrackingEnabled, onChanged: (val) => setState(() => _isTrackingEnabled = val), - activeColor: AppColors.menstrualPhase, + activeThumbColor: AppColors.menstrualPhase, ), ], ), - + if (_isTrackingEnabled) ...[ const Divider(height: 32), - + // Inventory Section Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -161,7 +164,8 @@ class _SuppliesSettingsScreenState extends ConsumerState onPressed: () => _addOrEditSupply(), icon: const Icon(Icons.add), label: const Text('Add Item'), - style: TextButton.styleFrom(foregroundColor: AppColors.menstrualPhase), + style: TextButton.styleFrom( + foregroundColor: AppColors.menstrualPhase), ), ], ), @@ -170,7 +174,7 @@ class _SuppliesSettingsScreenState extends ConsumerState Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.1), + color: Colors.grey.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Center( @@ -193,10 +197,12 @@ class _SuppliesSettingsScreenState extends ConsumerState tileColor: Theme.of(context).cardTheme.color, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - side: BorderSide(color: Colors.black.withOpacity(0.05)), + side: BorderSide( + color: Colors.black.withValues(alpha: 0.05)), ), leading: CircleAvatar( - backgroundColor: AppColors.menstrualPhase.withOpacity(0.1), + backgroundColor: + AppColors.menstrualPhase.withValues(alpha: 0.1), child: Text( item.count.toString(), style: GoogleFonts.outfit( @@ -205,10 +211,14 @@ class _SuppliesSettingsScreenState extends ConsumerState ), ), ), - title: Text(item.brand, style: GoogleFonts.outfit(fontWeight: FontWeight.w600)), - subtitle: Text(item.type.label, style: GoogleFonts.outfit(fontSize: 12)), + title: Text(item.brand, + style: + GoogleFonts.outfit(fontWeight: FontWeight.w600)), + subtitle: Text(item.type.label, + style: GoogleFonts.outfit(fontSize: 12)), trailing: IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.red), + icon: + const Icon(Icons.delete_outline, color: Colors.red), onPressed: () { setState(() { _supplies.removeAt(index); @@ -221,7 +231,7 @@ class _SuppliesSettingsScreenState extends ConsumerState ), const Divider(height: 32), - + // Typical Flow Text( 'Typical Flow Intensity', @@ -234,7 +244,9 @@ class _SuppliesSettingsScreenState extends ConsumerState const SizedBox(height: 8), Row( children: [ - Text('Light', style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray)), + Text('Light', + style: GoogleFonts.outfit( + fontSize: 12, color: AppColors.warmGray)), Expanded( child: Slider( value: _typicalFlow.toDouble(), @@ -242,42 +254,45 @@ class _SuppliesSettingsScreenState extends ConsumerState max: 5, divisions: 4, activeColor: AppColors.menstrualPhase, - onChanged: (val) => setState(() => _typicalFlow = val.round()), + onChanged: (val) => + setState(() => _typicalFlow = val.round()), ), ), - Text('Heavy', style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray)), + Text('Heavy', + style: GoogleFonts.outfit( + fontSize: 12, color: AppColors.warmGray)), ], ), Center( - child: Text( - '$_typicalFlow/5', - style: GoogleFonts.outfit( - fontWeight: FontWeight.bold, - color: AppColors.menstrualPhase - ) - ), + child: Text('$_typicalFlow/5', + style: GoogleFonts.outfit( + fontWeight: FontWeight.bold, + color: AppColors.menstrualPhase)), ), - + const SizedBox(height: 24), - + // Auto Deduct Toggle SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text( - 'Auto-deduct on Log', - style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: AppColors.charcoal), - ), - subtitle: Text( - 'Reduce count when you log a pad', - style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), - ), - value: _isAutoInventoryEnabled, - onChanged: (val) => setState(() => _isAutoInventoryEnabled = val), - activeColor: AppColors.menstrualPhase, + contentPadding: EdgeInsets.zero, + title: Text( + 'Auto-deduct on Log', + style: GoogleFonts.outfit( + fontWeight: FontWeight.w500, color: AppColors.charcoal), + ), + subtitle: Text( + 'Reduce count when you log a pad', + style: GoogleFonts.outfit( + fontSize: 12, color: AppColors.warmGray), + ), + value: _isAutoInventoryEnabled, + onChanged: (val) => + setState(() => _isAutoInventoryEnabled = val), + activeThumbColor: AppColors.menstrualPhase, ), const Divider(height: 32), - + Text( 'Timer Display Settings', style: GoogleFonts.outfit( @@ -289,35 +304,39 @@ class _SuppliesSettingsScreenState extends ConsumerState const SizedBox(height: 8), SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text( - 'Show Minutes', - style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: AppColors.charcoal), - ), - value: _showPadTimerMinutes, - onChanged: (val) { - setState(() { - _showPadTimerMinutes = val; - if (!val) { - _showPadTimerSeconds = false; - } - }); - }, - activeColor: AppColors.menstrualPhase, + contentPadding: EdgeInsets.zero, + title: Text( + 'Show Minutes', + style: GoogleFonts.outfit( + fontWeight: FontWeight.w500, color: AppColors.charcoal), + ), + value: _showPadTimerMinutes, + onChanged: (val) { + setState(() { + _showPadTimerMinutes = val; + if (!val) { + _showPadTimerSeconds = false; + } + }); + }, + activeThumbColor: AppColors.menstrualPhase, ), SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text( - 'Show Seconds', - style: GoogleFonts.outfit( - fontWeight: FontWeight.w500, - color: _showPadTimerMinutes ? AppColors.charcoal : AppColors.warmGray - ), - ), - value: _showPadTimerSeconds, - onChanged: _showPadTimerMinutes ? (val) => setState(() => _showPadTimerSeconds = val) : null, - activeColor: AppColors.menstrualPhase, + contentPadding: EdgeInsets.zero, + title: Text( + 'Show Seconds', + style: GoogleFonts.outfit( + fontWeight: FontWeight.w500, + color: _showPadTimerMinutes + ? AppColors.charcoal + : AppColors.warmGray), + ), + value: _showPadTimerSeconds, + onChanged: _showPadTimerMinutes + ? (val) => setState(() => _showPadTimerSeconds = val) + : null, + activeThumbColor: AppColors.menstrualPhase, ), ], ], @@ -345,11 +364,12 @@ class _SupplyDialogState extends State<_SupplyDialog> { @override void initState() { super.initState(); - _brandController = TextEditingController(text: widget.initialItem?.brand ?? ''); + _brandController = + TextEditingController(text: widget.initialItem?.brand ?? ''); _type = widget.initialItem?.type ?? PadType.regular; _count = widget.initialItem?.count ?? 0; } - + @override void dispose() { _brandController.dispose(); @@ -371,11 +391,13 @@ class _SupplyDialogState extends State<_SupplyDialog> { ), const SizedBox(height: 16), DropdownButtonFormField( - value: _type, - items: PadType.values.map((t) => DropdownMenuItem( - value: t, - child: Text(t.label), - )).toList(), + initialValue: _type, + items: PadType.values + .map((t) => DropdownMenuItem( + value: t, + child: Text(t.label), + )) + .toList(), onChanged: (val) => setState(() => _type = val!), decoration: const InputDecoration(labelText: 'Type'), ), @@ -383,14 +405,18 @@ class _SupplyDialogState extends State<_SupplyDialog> { TextField( keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: 'Quantity'), - controller: TextEditingController(text: _count.toString()), // Hacky for demo, binding needed properly + controller: TextEditingController( + text: _count + .toString()), // Hacky for demo, binding needed properly onChanged: (val) => _count = int.tryParse(val) ?? 0, ), ], ), ), actions: [ - TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel')), ElevatedButton( onPressed: () { if (_brandController.text.isEmpty) return; diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index cfe9673..66a5e75 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -9,13 +9,15 @@ import '../providers/user_provider.dart'; import 'husband/husband_home_screen.dart'; class SplashScreen extends ConsumerStatefulWidget { - const SplashScreen({super.key}); + final bool isStartup; + const SplashScreen({super.key, this.isStartup = false}); @override ConsumerState createState() => _SplashScreenState(); } -class _SplashScreenState extends ConsumerState with SingleTickerProviderStateMixin { +class _SplashScreenState extends ConsumerState + with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _fadeAnimation; late Animation _scaleAnimation; @@ -44,16 +46,18 @@ class _SplashScreenState extends ConsumerState with SingleTickerPr _controller.forward(); - // Navigate after splash - Future.delayed(const Duration(milliseconds: 1200), () { - _navigateToNextScreen(); - }); + // Navigate after splash ONLY if not managed by AppStartupWidget + if (!widget.isStartup) { + Future.delayed(const Duration(milliseconds: 1200), () { + _navigateToNextScreen(); + }); + } } void _navigateToNextScreen() { final user = ref.read(userProfileProvider); final hasProfile = user != null; - + Widget nextScreen; if (!hasProfile) { nextScreen = const OnboardingScreen(); @@ -103,7 +107,7 @@ class _SplashScreenState extends ConsumerState with SingleTickerPr gradient: LinearGradient( colors: [ AppColors.blushPink, - AppColors.rose.withOpacity(0.8), + AppColors.rose.withValues(alpha: 0.8), ], begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -111,7 +115,7 @@ class _SplashScreenState extends ConsumerState with SingleTickerPr borderRadius: BorderRadius.circular(30), boxShadow: [ BoxShadow( - color: AppColors.rose.withOpacity(0.3), + color: AppColors.rose.withValues(alpha: 0.3), blurRadius: 20, offset: const Offset(0, 10), ), @@ -124,7 +128,7 @@ class _SplashScreenState extends ConsumerState with SingleTickerPr ), ), const SizedBox(height: 24), - + // App Name placeholder Text( 'Period Tracker', @@ -135,7 +139,7 @@ class _SplashScreenState extends ConsumerState with SingleTickerPr ), ), const SizedBox(height: 8), - + // Tagline Text( 'Faith-Centered Wellness', @@ -147,7 +151,7 @@ class _SplashScreenState extends ConsumerState with SingleTickerPr ), ), const SizedBox(height: 48), - + // Scripture Padding( padding: const EdgeInsets.symmetric(horizontal: 48), diff --git a/lib/services/bible_utils.dart b/lib/services/bible_utils.dart index 6ac602b..22f4124 100644 --- a/lib/services/bible_utils.dart +++ b/lib/services/bible_utils.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/user_profile.dart'; -import '../models/scripture.dart'; import '../theme/app_theme.dart'; import '../providers/user_provider.dart'; class BibleUtils { - static Future showTranslationPicker(BuildContext context, WidgetRef ref) async { + static Future showTranslationPicker( + BuildContext context, WidgetRef ref) async { final user = ref.read(userProfileProvider); if (user == null) return; @@ -28,21 +28,21 @@ class BibleUtils { ), ), ...BibleTranslation.values.map((t) => ListTile( - title: Text(t.label), - trailing: user.bibleTranslation == t - ? const Icon(Icons.check, color: AppColors.sageGreen) - : null, - onTap: () => Navigator.pop(context, t), - )), + title: Text(t.label), + trailing: user.bibleTranslation == t + ? const Icon(Icons.check, color: AppColors.sageGreen) + : null, + onTap: () => Navigator.pop(context, t), + )), ], ), ), ); if (selected != null) { - await ref.read(userProfileProvider.notifier).updateProfile( - user.copyWith(bibleTranslation: selected) - ); + await ref + .read(userProfileProvider.notifier) + .updateProfile(user.copyWith(bibleTranslation: selected)); } } } diff --git a/lib/services/bible_xml_parser.dart b/lib/services/bible_xml_parser.dart index e5607bf..53a9b87 100644 --- a/lib/services/bible_xml_parser.dart +++ b/lib/services/bible_xml_parser.dart @@ -1,7 +1,6 @@ import 'package:flutter/services.dart' show rootBundle; import 'package:flutter/foundation.dart'; import 'package:xml/xml.dart'; -import '../models/scripture.dart'; // Assuming Scripture model might need BibleTranslation // Helper for background XML parsing XmlDocument parseXmlString(String xml) { @@ -14,18 +13,24 @@ class BibleXmlParser { static const Map _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', + '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', + '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', + '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', @@ -50,7 +55,6 @@ class BibleXmlParser { }; } - // Cache for parsed XML documents to avoid reloading/reparsing static final Map _xmlCache = {}; @@ -59,8 +63,8 @@ class BibleXmlParser { if (_xmlCache.containsKey(assetPath)) { return _xmlCache[assetPath]!; } - - print('Loading and parsing XML asset: $assetPath'); // Debug log + + debugPrint('Loading and parsing XML asset: $assetPath'); // Debug log final String xmlString = await rootBundle.loadString(assetPath); final document = await compute(parseXmlString, xmlString); _xmlCache[assetPath] = document; @@ -71,13 +75,11 @@ class BibleXmlParser { /// Supports two schemas: /// 1. /// 2. - /// Extracts a specific verse from a parsed XML document. - /// Supports two schemas: - /// 1. - /// 2. - String? getVerseFromXml(XmlDocument document, String bookName, int chapterNum, int verseNum) { + String? getVerseFromXml( + XmlDocument document, String bookName, int chapterNum, int verseNum) { // Standardize book name for lookup - String lookupBookName = _bookAbbreviations[bookName.toLowerCase()] ?? bookName; + String lookupBookName = + _bookAbbreviations[bookName.toLowerCase()] ?? bookName; // Use root element to avoid full document search XmlElement root = document.rootElement; @@ -88,7 +90,7 @@ class BibleXmlParser { (element) { final nameAttr = element.getAttribute('bname'); return nameAttr?.toLowerCase() == lookupBookName.toLowerCase() || - nameAttr?.toLowerCase() == bookName.toLowerCase(); + nameAttr?.toLowerCase() == bookName.toLowerCase(); }, orElse: () => XmlElement(XmlName('notfound')), ); @@ -99,54 +101,54 @@ class BibleXmlParser { (element) { final nameAttr = element.getAttribute('n'); return nameAttr?.toLowerCase() == lookupBookName.toLowerCase() || - nameAttr?.toLowerCase() == bookName.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 + // debugPrint('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')), - ); + (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')), - ); + (element) => element.getAttribute('n') == chapterNum.toString(), + orElse: () => XmlElement(XmlName('notfound')), + ); } if (chapterElement.name.local == 'notfound') { - // print('Chapter "$chapterNum" not found for book "$bookName".'); + // debugPrint('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')), - ); + (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')), - ); + (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".'); + // debugPrint('Verse "$verseNum" not found for Chapter "$chapterNum", book "$bookName".'); return null; } @@ -158,7 +160,7 @@ class BibleXmlParser { Future getVerseFromAsset(String assetPath, String reference) async { final parsedRef = parseReference(reference); if (parsedRef == null) { - print('Invalid reference format: $reference'); + debugPrint('Invalid reference format: $reference'); return null; } diff --git a/lib/services/cycle_service.dart b/lib/services/cycle_service.dart index a95bda8..bb1b412 100644 --- a/lib/services/cycle_service.dart +++ b/lib/services/cycle_service.dart @@ -36,9 +36,10 @@ class CycleInfo { class CycleService { /// Calculates the current cycle information based on user profile /// Calculates the current cycle information based on user profile and cycle entries - static CycleInfo calculateCycleInfo(UserProfile? user, List entries) { + static CycleInfo calculateCycleInfo( + UserProfile? user, List entries) { if (user == null) { - return CycleInfo( + return CycleInfo( phase: CyclePhase.follicular, dayOfCycle: 1, daysUntilPeriod: 28, @@ -51,69 +52,70 @@ class CycleService { // Find the most recent period start from entries if available and more recent // We look for a sequence of period days and take the first one if (entries.isNotEmpty) { - final sortedEntries = List.from(entries)..sort((a, b) => b.date.compareTo(a.date)); - + final sortedEntries = List.from(entries) + ..sort((a, b) => b.date.compareTo(a.date)); + for (var entry in sortedEntries) { if (entry.isPeriodDay) { - // Check if this is a "start" of a period (previous day was not period or no entry) - // Simplified logic: Just take the most recent period day found and assume it's part of the current/last period - // A better approach for "Day 1" is to find the First day of the contiguous block. - - // However, for basic calculation, if we find a period day at Date X, - // and today is Date Y. - // If X is very recent, we are in the period. - - // Correct logic: Identify the START DATE of the last period group. - // 1. Find the latest period entry. - // 2. Look backwards from there as long as there are consecutive period days. - - DateTime potentialStart = entry.date; - - // Check if we have a period day "tomorrow" relative to this entry? No, we are iterating backwards (descending). - // So if we found a period day, we need to check if the NEXT entry (which is earlier in time) is also a period day. - // If so, that earlier day is the better candidate for "Start". - - // Let's iterate linearly. - // Since we sorted DESC, `entry` is the LATEST period day. - // We need to see if there are consecutive period days before it. - - // But wait, the user might have logged Day 1, Day 2, Day 3. - // `entry` will be Day 3. - // We want Day 1. - - // Let's try a different approach: - // Get all period days sorted DESC. - final periodDays = sortedEntries.where((e) => e.isPeriodDay).toList(); - - if (periodDays.isNotEmpty) { - // Take the latest block - DateTime latestParams = periodDays.first.date; - - // Now find the "start" of this block - // We iterate backwards from the *latest* date found - - DateTime currentSearch = latestParams; - DateTime startOfBlock = latestParams; - - // Check if we have an entry for the day before - bool foundPrevious = true; - while(foundPrevious) { - final dayBefore = currentSearch.subtract(const Duration(days: 1)); - final hasDayBefore = periodDays.any((e) => DateUtils.isSameDay(e.date, dayBefore)); - if (hasDayBefore) { - currentSearch = dayBefore; - startOfBlock = dayBefore; - } else { - foundPrevious = false; - } - } - - // If this calculated start is more recent than the user profile one, use it - if (lastPeriodStart == null || startOfBlock.isAfter(lastPeriodStart)) { - lastPeriodStart = startOfBlock; - } - } - break; // We only care about the most recent period block + // Check if this is a "start" of a period (previous day was not period or no entry) + // Simplified logic: Just take the most recent period day found and assume it's part of the current/last period + // A better approach for "Day 1" is to find the First day of the contiguous block. + + // However, for basic calculation, if we find a period day at Date X, + // and today is Date Y. + // If X is very recent, we are in the period. + + // Correct logic: Identify the START DATE of the last period group. + // 1. Find the latest period entry. + // 2. Look backwards from there as long as there are consecutive period days. + + // Check if we have a period day "tomorrow" relative to this entry? No, we are iterating backwards (descending). + // So if we found a period day, we need to check if the NEXT entry (which is earlier in time) is also a period day. + // If so, that earlier day is the better candidate for "Start". + + // Let's iterate linearly. + // Since we sorted DESC, `entry` is the LATEST period day. + // We need to see if there are consecutive period days before it. + + // But wait, the user might have logged Day 1, Day 2, Day 3. + // `entry` will be Day 3. + // We want Day 1. + + // Let's try a different approach: + // Get all period days sorted DESC. + final periodDays = sortedEntries.where((e) => e.isPeriodDay).toList(); + + if (periodDays.isNotEmpty) { + // Take the latest block + DateTime latestParams = periodDays.first.date; + + // Now find the "start" of this block + // We iterate backwards from the *latest* date found + + DateTime currentSearch = latestParams; + DateTime startOfBlock = latestParams; + + // Check if we have an entry for the day before + bool foundPrevious = true; + while (foundPrevious) { + final dayBefore = currentSearch.subtract(const Duration(days: 1)); + final hasDayBefore = + periodDays.any((e) => DateUtils.isSameDay(e.date, dayBefore)); + if (hasDayBefore) { + currentSearch = dayBefore; + startOfBlock = dayBefore; + } else { + foundPrevious = false; + } + } + + // If this calculated start is more recent than the user profile one, use it + if (lastPeriodStart == null || + startOfBlock.isAfter(lastPeriodStart)) { + lastPeriodStart = startOfBlock; + } + } + break; // We only care about the most recent period block } } } @@ -126,38 +128,41 @@ class CycleService { isPeriodExpected: false, ); } - + // Check if the calculated last period is in the future (invalid state validation) if (lastPeriodStart.isAfter(DateTime.now())) { - // Fallback to today if data is weird, or just use it (maybe user logged future?) - // Let's stick to standard logic: + // Fallback to today if data is weird, or just use it (maybe user logged future?) + // Let's stick to standard logic: } final cycleLength = user.averageCycleLength; final now = DateTime.now(); - + // Normalize dates to midnight for accurate day counting final startOfToday = DateTime(now.year, now.month, now.day); - final startOfCycle = DateTime(lastPeriodStart.year, lastPeriodStart.month, lastPeriodStart.day); - - final daysSinceLastPeriod = startOfToday.difference(startOfCycle).inDays + 1; - + final startOfCycle = DateTime( + lastPeriodStart.year, lastPeriodStart.month, lastPeriodStart.day); + + final daysSinceLastPeriod = + startOfToday.difference(startOfCycle).inDays + 1; + // If negative (future date), handle gracefully if (daysSinceLastPeriod < 1) { - return CycleInfo( + return CycleInfo( phase: CyclePhase.follicular, dayOfCycle: 1, daysUntilPeriod: cycleLength, isPeriodExpected: false, ); } - + // Handle cases where last period was long ago (more than one cycle) final dayOfCycle = ((daysSinceLastPeriod - 1) % cycleLength) + 1; final daysUntilPeriod = cycleLength - dayOfCycle; CyclePhase phase; - if (dayOfCycle <= user.averagePeriodLength) { // Use variable period length + if (dayOfCycle <= user.averagePeriodLength) { + // Use variable period length phase = CyclePhase.menstrual; } else if (dayOfCycle <= 13) { phase = CyclePhase.follicular; @@ -180,13 +185,14 @@ class CycleService { if (user == null || user.lastPeriodStartDate == null) return null; final lastPeriodStart = user.lastPeriodStartDate!; - + // Normalize dates final checkDate = DateTime(date.year, date.month, date.day); - final startCycle = DateTime(lastPeriodStart.year, lastPeriodStart.month, lastPeriodStart.day); + final startCycle = DateTime( + lastPeriodStart.year, lastPeriodStart.month, lastPeriodStart.day); final daysDifference = checkDate.difference(startCycle).inDays; - + // If date is before the last known period, we can't reliably predict using this simple logic // (though in reality we could project backwards, but let's stick to forward/current) if (daysDifference < 0) return null; @@ -201,7 +207,8 @@ class CycleService { } /// Predicts period days for the next [months] months - static List predictNextPeriodDays(UserProfile? user, {int months = 12}) { + static List predictNextPeriodDays(UserProfile? user, + {int months = 12}) { if (user == null || user.lastPeriodStartDate == null) return []; final predictedDays = []; @@ -209,12 +216,12 @@ class CycleService { final cycleLength = user.averageCycleLength; final periodLength = user.averagePeriodLength; - // Start predicting from the NEXT cycle if the current one is finished, + // Start predicting from the NEXT cycle if the current one is finished, // or just project out from the last start date. // We want to list all future period days. - + DateTime currentCycleStart = lastPeriodStart; - + // Project forward for roughly 'months' months // A safe upper bound for loop is months * 30 days final limitDate = DateTime.now().add(Duration(days: months * 30)); @@ -227,11 +234,11 @@ class CycleService { predictedDays.add(periodDay); } } - + // Move to next cycle currentCycleStart = currentCycleStart.add(Duration(days: cycleLength)); } - + return predictedDays; } diff --git a/lib/services/health_service.dart b/lib/services/health_service.dart index 1736886..e5215cc 100644 --- a/lib/services/health_service.dart +++ b/lib/services/health_service.dart @@ -1,6 +1,4 @@ import 'package:health/health.dart'; -import 'package:collection/collection.dart'; -import 'dart:io'; import 'package:flutter/foundation.dart'; import '../models/cycle_entry.dart'; @@ -10,14 +8,12 @@ class HealthService { HealthService._internal(); final Health _health = Health(); - + // ignore: unused_field List _requestedTypes = []; - // TODO: Fix HealthDataType for menstruation in newer health package versions static const List _menstruationDataTypes = [ - // HealthDataType.MENSTRUATION - Not found in recent versions? - HealthDataType.STEPS, // Placeholder to avoid compile error + HealthDataType.MENSTRUATION_FLOW, ]; Future requestAuthorization(List types) async { @@ -37,9 +33,10 @@ class HealthService { Future writeMenstruationData(List entries) async { // This feature is currently disabled until compatible HealthDataType is identified - debugPrint("writeMenstruationData: Currently disabled due to package version incompatibility."); + debugPrint( + "writeMenstruationData: Currently disabled due to package version incompatibility."); return false; - + /* final periodEntries = entries.where((entry) => entry.isPeriodDay).toList(); diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 21e873b..a145f80 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -28,7 +28,7 @@ class NotificationService { const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); - final DarwinInitializationSettings initializationSettingsDarwin = + const DarwinInitializationSettings initializationSettingsDarwin = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, @@ -36,10 +36,10 @@ class NotificationService { ); // Linux initialization (optional, but good for completeness) - final LinuxInitializationSettings initializationSettingsLinux = + const LinuxInitializationSettings initializationSettingsLinux = LinuxInitializationSettings(defaultActionName: 'Open notification'); - final InitializationSettings initializationSettings = + const InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsDarwin, @@ -66,7 +66,8 @@ class NotificationService { if (kIsWeb) { // Web platform limitation: Background scheduling is complex. // For this demo/web preview, we'll just log it or rely on the UI confirmation. - print('Web Notification Scheduled: $title - $body at $scheduledDate'); + debugPrint( + 'Web Notification Scheduled: $title - $body at $scheduledDate'); return; } @@ -100,7 +101,7 @@ class NotificationService { String? channelName, }) async { if (kIsWeb) { - print('Web Local Notification: $title - $body'); + debugPrint('Web Local Notification: $title - $body'); return; } const AndroidNotificationDetails androidNotificationDetails = diff --git a/lib/services/pdf_service.dart b/lib/services/pdf_service.dart index 3b82377..8195443 100644 --- a/lib/services/pdf_service.dart +++ b/lib/services/pdf_service.dart @@ -1,105 +1,109 @@ -import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:printing/printing.dart'; -import 'package:intl/intl.dart'; import '../models/user_profile.dart'; import '../models/cycle_entry.dart'; +import 'package:intl/intl.dart'; class PdfService { - static Future generateCycleReport(UserProfile? user, List entries) async { + static Future generateCycleReport( + UserProfile user, List entries) async { final pdf = pw.Document(); - final font = await PdfGoogleFonts.outfitRegular(); - final boldFont = await PdfGoogleFonts.outfitBold(); + + final logo = pw.MemoryImage( + (await rootBundle.load('assets/images/logo.png')).buffer.asUint8List(), + ); // Group entries by month - final entriesByMonth = >{}; + final Map> groupedEntries = {}; for (var entry in entries) { final month = DateFormat('MMMM yyyy').format(entry.date); - if (!entriesByMonth.containsKey(month)) { - entriesByMonth[month] = []; + if (!groupedEntries.containsKey(month)) { + groupedEntries[month] = []; } - entriesByMonth[month]!.add(entry); + groupedEntries[month]!.add(entry); } + // Sort months chronologically (most recent first) + final sortedMonths = groupedEntries.keys.toList() + ..sort((a, b) { + final dateA = DateFormat('MMMM yyyy').parse(a); + final dateB = DateFormat('MMMM yyyy').parse(b); + return dateB.compareTo(dateA); + }); + pdf.addPage( pw.MultiPage( pageFormat: PdfPageFormat.a4, - theme: pw.ThemeData.withFont( - base: font, - bold: boldFont, - ), + margin: const pw.EdgeInsets.all(32), build: (pw.Context context) { return [ - pw.Header( - level: 0, - child: pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Text('Cycle Report', style: pw.TextStyle(fontSize: 24, fontWeight: pw.FontWeight.bold)), - pw.Text(DateFormat.yMMMd().format(DateTime.now()), style: const pw.TextStyle(color: PdfColors.grey)), - ], - ), - ), - if (user != null) - pw.Padding( - padding: const pw.EdgeInsets.only(bottom: 20), - child: pw.Column( + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Text('Name: ${user.name}'), - pw.Text('Average Cycle Length: ${user.averageCycleLength} days'), - pw.Text('Average Period Length: ${user.averagePeriodLength} days'), + pw.Text('Cycle History Report', + style: pw.TextStyle( + fontSize: 24, + fontWeight: pw.FontWeight.bold, + color: PdfColors.blueGrey900)), + pw.Text('Generated for ${user.name}', + style: const pw.TextStyle( + fontSize: 14, color: PdfColors.blueGrey600)), + pw.Text( + 'Date Range: ${DateFormat('MMM yyyy').format(entries.last.date)} - ${DateFormat('MMM yyyy').format(entries.first.date)}', + style: const pw.TextStyle( + fontSize: 12, color: PdfColors.blueGrey400)), ], ), - ), - - ...entriesByMonth.entries.map((entry) { - final month = entry.key; - final monthEntries = entry.value; - // Sort by date - monthEntries.sort((a, b) => a.date.compareTo(b.date)); - - return pw.Column( + pw.SizedBox(height: 50, width: 50, child: pw.Image(logo)), + ], + ), + pw.SizedBox(height: 30), + for (var month in sortedMonths) ...[ + pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.SizedBox(height: 10), - pw.Text(month, style: pw.TextStyle(fontSize: 18, fontWeight: pw.FontWeight.bold, color: PdfColors.blueGrey800)), + pw.Text(month, + style: pw.TextStyle( + fontSize: 18, + fontWeight: pw.FontWeight.bold, + color: PdfColors.blueGrey800)), pw.SizedBox(height: 5), - pw.Table.fromTextArray( + pw.TableHelper.fromTextArray( context: context, headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold), headers: ['Date', 'Phase', 'Details', 'Notes'], - data: monthEntries.map((e) { - final details = []; - if (e.isPeriodDay) details.add('Period'); - if (e.mood != null) details.add('Mood: ${e.mood!.label}'); - if (e.symptomCount > 0) details.add('${e.symptomCount} symptoms'); - + data: groupedEntries[month]!.map((e) { return [ - DateFormat('d, E').format(e.date), - '${e.isPeriodDay ? "Menstrual" : "-"}', // Simplified for report - details.join(', '), - e.notes ?? '', + DateFormat('MMM d').format(e.date), + e.isPeriodDay + ? 'Period' + : (e.flowIntensity == FlowIntensity.spotting + ? 'Spotting' + : 'Other'), + e.flowIntensity != null + ? 'Flow: ${e.flowIntensity.toString().split('.').last}' + : '-', + e.notes ?? '-', ]; }).toList(), - columnWidths: { - 0: const pw.FlexColumnWidth(1), - 1: const pw.FlexColumnWidth(1), - 2: const pw.FlexColumnWidth(2), - 3: const pw.FlexColumnWidth(2), - }, ), - pw.SizedBox(height: 15), + pw.SizedBox(height: 20), ], - ); - }), + ), + ], ]; }, ), ); - await Printing.sharePdf(bytes: await pdf.save(), filename: 'cycle_report.pdf'); + await Printing.layoutPdf( + onLayout: (PdfPageFormat format) async => pdf.save(), + ); } } diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index 1ae2c09..06120db 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -131,7 +131,7 @@ class AppTheme { cardTheme: CardThemeData( color: Colors.white, elevation: 2, - shadowColor: AppColors.charcoal.withOpacity(0.1), + shadowColor: AppColors.charcoal.withValues(alpha: 0.1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), @@ -186,11 +186,13 @@ class AppTheme { fillColor: Colors.white, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: AppColors.lightGray.withOpacity(0.5)), + borderSide: + BorderSide(color: AppColors.lightGray.withValues(alpha: 0.5)), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: AppColors.lightGray.withOpacity(0.5)), + borderSide: + BorderSide(color: AppColors.lightGray.withValues(alpha: 0.5)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), @@ -231,15 +233,15 @@ class AppTheme { // Slider Theme sliderTheme: SliderThemeData( activeTrackColor: accentColor, - inactiveTrackColor: AppColors.lightGray.withOpacity(0.3), + inactiveTrackColor: AppColors.lightGray.withValues(alpha: 0.3), thumbColor: accentColor, - overlayColor: accentColor.withOpacity(0.2), + overlayColor: accentColor.withValues(alpha: 0.2), trackHeight: 4, ), // Divider dividerTheme: DividerThemeData( - color: AppColors.lightGray.withOpacity(0.3), + color: AppColors.lightGray.withValues(alpha: 0.3), thickness: 1, space: 24, ), @@ -262,7 +264,7 @@ class AppTheme { onSecondary: Colors.white, onSurface: Colors.white, onSurfaceVariant: Colors.white70, - outline: Colors.white.withOpacity(0.1), + outline: Colors.white.withValues(alpha: 0.1), ), // Scaffold @@ -338,11 +340,12 @@ class AppTheme { // Card Theme cardTheme: CardThemeData( color: const Color(0xFF1E1E1E), - elevation: 0, // Material 3 uses color/opacity for elevation in dark mode + elevation: + 0, // Material 3 uses color/opacity for elevation in dark mode shadowColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), - side: BorderSide(color: Colors.white.withOpacity(0.05)), + side: BorderSide(color: Colors.white.withValues(alpha: 0.05)), ), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), @@ -367,7 +370,8 @@ class AppTheme { outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( foregroundColor: accentColor, - side: BorderSide(color: accentColor.withOpacity(0.5), width: 1.5), + side: + BorderSide(color: accentColor.withValues(alpha: 0.5), width: 1.5), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), @@ -385,11 +389,11 @@ class AppTheme { fillColor: const Color(0xFF1E1E1E), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.white.withOpacity(0.1)), + borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.1)), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.white.withOpacity(0.1)), + borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.1)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), @@ -423,9 +427,9 @@ class AppTheme { // Slider Theme sliderTheme: SliderThemeData( activeTrackColor: accentColor, - inactiveTrackColor: Colors.white.withOpacity(0.1), + inactiveTrackColor: Colors.white.withValues(alpha: 0.1), thumbColor: accentColor, - overlayColor: accentColor.withOpacity(0.2), + overlayColor: accentColor.withValues(alpha: 0.2), trackHeight: 4, tickMarkShape: const RoundSliderTickMarkShape(), activeTickMarkColor: Colors.white24, @@ -434,7 +438,7 @@ class AppTheme { // Divider dividerTheme: DividerThemeData( - color: Colors.white.withOpacity(0.05), + color: Colors.white.withValues(alpha: 0.05), thickness: 1, space: 24, ), diff --git a/lib/widgets/cycle_ring.dart b/lib/widgets/cycle_ring.dart index de0306d..1d19bd7 100644 --- a/lib/widgets/cycle_ring.dart +++ b/lib/widgets/cycle_ring.dart @@ -97,12 +97,12 @@ class _CycleRingState extends State horizontal: 12, vertical: 6), decoration: BoxDecoration( color: _getPhaseColor(widget.phase) - .withOpacity(isDark ? 0.3 : 0.2), + .withValues(alpha: isDark ? 0.3 : 0.2), borderRadius: BorderRadius.circular(20), border: isDark ? Border.all( color: _getPhaseColor(widget.phase) - .withOpacity(0.5)) + .withValues(alpha: 0.5)) : null, ), child: Row( @@ -133,8 +133,9 @@ class _CycleRingState extends State : 'Period expected', style: Theme.of(context).textTheme.bodySmall?.copyWith( fontSize: 12, - color: - Theme.of(context).colorScheme.onSurfaceVariant, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, ), ), ], @@ -181,8 +182,8 @@ class _CycleRingPainter extends CustomPainter { // Background arc final bgPaint = Paint() - ..color = - (isDark ? Colors.white : AppColors.lightGray).withOpacity(isDark ? 0.05 : 0.1) + ..color = (isDark ? Colors.white : AppColors.lightGray) + .withValues(alpha: isDark ? 0.05 : 0.1) ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..strokeCap = StrokeCap.round; @@ -235,16 +236,12 @@ class _CycleRingPainter extends CustomPainter { return [ AppColors.sageGreen, AppColors.follicularPhase, - AppColors.sageGreen.withOpacity(0.7) + AppColors.sageGreen.withValues(alpha: 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]; } } diff --git a/lib/widgets/pad_settings_dialog.dart b/lib/widgets/pad_settings_dialog.dart index 9ed79b0..d11b901 100644 --- a/lib/widgets/pad_settings_dialog.dart +++ b/lib/widgets/pad_settings_dialog.dart @@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../theme/app_theme.dart'; import '../../providers/user_provider.dart'; -import '../../models/user_profile.dart'; // Import UserProfile class PadSettingsDialog extends ConsumerStatefulWidget { const PadSettingsDialog({super.key}); @@ -16,12 +15,12 @@ class _PadSettingsDialogState extends ConsumerState { bool _isTrackingEnabled = false; int _typicalFlow = 2; // Mediumish int _padAbsorbency = 3; // Regular - + // Inventory State int _padInventoryCount = 0; int _lowInventoryThreshold = 5; bool _isAutoInventoryEnabled = true; - + final TextEditingController _brandController = TextEditingController(); @override @@ -32,12 +31,12 @@ class _PadSettingsDialogState extends ConsumerState { _isTrackingEnabled = user.isPadTrackingEnabled; _typicalFlow = user.typicalFlowIntensity ?? 2; _padAbsorbency = user.padAbsorbency ?? 3; - + // Init Inventory _padInventoryCount = user.padInventoryCount; _lowInventoryThreshold = user.lowInventoryThreshold; _isAutoInventoryEnabled = user.isAutoInventoryEnabled; - + _brandController.text = user.padBrand ?? ''; } } @@ -58,12 +57,18 @@ class _PadSettingsDialogState extends ConsumerState { padInventoryCount: _padInventoryCount, lowInventoryThreshold: _lowInventoryThreshold, isAutoInventoryEnabled: _isAutoInventoryEnabled, - lastInventoryUpdate: (_padInventoryCount != (user.padInventoryCount)) ? DateTime.now() : user.lastInventoryUpdate, - padBrand: _brandController.text.trim().isEmpty ? null : _brandController.text.trim(), + lastInventoryUpdate: (_padInventoryCount != (user.padInventoryCount)) + ? DateTime.now() + : user.lastInventoryUpdate, + padBrand: _brandController.text.trim().isEmpty + ? null + : _brandController.text.trim(), ); - - await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); - + + await ref + .read(userProfileProvider.notifier) + .updateProfile(updatedProfile); + if (mounted) { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( @@ -92,7 +97,7 @@ class _PadSettingsDialogState extends ConsumerState { ), ), const SizedBox(height: 24), - + // Toggle Row( children: [ @@ -108,14 +113,14 @@ class _PadSettingsDialogState extends ConsumerState { Switch( value: _isTrackingEnabled, onChanged: (val) => setState(() => _isTrackingEnabled = val), - activeColor: AppColors.menstrualPhase, + activeThumbColor: AppColors.menstrualPhase, ), ], ), - + if (_isTrackingEnabled) ...[ const Divider(height: 32), - + // Typical Flow Text( 'Typical Flow Intensity', @@ -128,34 +133,40 @@ class _PadSettingsDialogState extends ConsumerState { const SizedBox(height: 8), Row( children: [ - Text('Light', style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray)), + Text('Light', + style: GoogleFonts.outfit( + fontSize: 12, color: AppColors.warmGray)), Expanded( child: Slider( value: _typicalFlow.toDouble(), min: 1, max: 5, divisions: 4, - activeColor: AppColors.menstrualPhase, - onChanged: (val) => setState(() => _typicalFlow = val.round()), + activeColor: AppColors + .menstrualPhase, // Slider still uses activeColor in many versions, check specifically. + // If it's SwitchListTile, it's activeColor, Slider is activeColor. + // Actually, let's keep it as is if it's not deprecated for Slider. + + onChanged: (val) => + setState(() => _typicalFlow = val.round()), ), ), - Text('Heavy', style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray)), + Text('Heavy', + style: GoogleFonts.outfit( + fontSize: 12, color: AppColors.warmGray)), ], ), Center( - child: Text( - '$_typicalFlow/5', - style: GoogleFonts.outfit( - fontWeight: FontWeight.bold, - color: AppColors.menstrualPhase - ) - ), + child: Text('$_typicalFlow/5', + style: GoogleFonts.outfit( + fontWeight: FontWeight.bold, + color: AppColors.menstrualPhase)), ), - + const SizedBox(height: 20), - + // Pad Absorbency - Text( + Text( 'Pad Absorbency/Capacity', style: GoogleFonts.outfit( fontSize: 14, @@ -166,11 +177,15 @@ class _PadSettingsDialogState extends ConsumerState { const SizedBox(height: 4), Text( 'Regular(3), Super(4), Overnight(5)', - style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray.withOpacity(0.8)), + style: GoogleFonts.outfit( + fontSize: 12, + color: AppColors.warmGray.withValues(alpha: 0.8)), ), Row( children: [ - Text('Low', style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray)), + Text('Low', + style: GoogleFonts.outfit( + fontSize: 12, color: AppColors.warmGray)), Expanded( child: Slider( value: _padAbsorbency.toDouble(), @@ -178,24 +193,24 @@ class _PadSettingsDialogState extends ConsumerState { max: 5, divisions: 4, activeColor: AppColors.menstrualPhase, - onChanged: (val) => setState(() => _padAbsorbency = val.round()), + onChanged: (val) => + setState(() => _padAbsorbency = val.round()), ), ), - Text('High', style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray)), + Text('High', + style: GoogleFonts.outfit( + fontSize: 12, color: AppColors.warmGray)), ], ), Center( - child: Text( - '$_padAbsorbency/5', - style: GoogleFonts.outfit( - fontWeight: FontWeight.bold, - color: AppColors.menstrualPhase - ) - ), + child: Text('$_padAbsorbency/5', + style: GoogleFonts.outfit( + fontWeight: FontWeight.bold, + color: AppColors.menstrualPhase)), ), - + const SizedBox(height: 20), - + // Brand Text( 'Preferred Brand', @@ -211,12 +226,13 @@ class _PadSettingsDialogState extends ConsumerState { decoration: InputDecoration( hintText: 'e.g., Always Infinity, Cora, Period Undies...', filled: true, - fillColor: AppColors.warmCream.withOpacity(0.5), + fillColor: AppColors.warmCream.withValues(alpha: 0.5), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), ), @@ -227,9 +243,10 @@ class _PadSettingsDialogState extends ConsumerState { // Inventory Management Header Row( children: [ - Icon(Icons.inventory_2_outlined, size: 20, color: AppColors.navyBlue), - const SizedBox(width: 8), - Text( + const Icon(Icons.inventory_2_outlined, + size: 20, color: AppColors.navyBlue), + const SizedBox(width: 8), + Text( 'Inventory Tracking', style: GoogleFonts.outfit( fontSize: 16, @@ -250,38 +267,45 @@ class _PadSettingsDialogState extends ConsumerState { children: [ Text( 'Current Stock', - style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: AppColors.warmGray), + style: GoogleFonts.outfit( + fontWeight: FontWeight.w500, + color: AppColors.warmGray), ), Text( 'Pad Inventory', - style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray.withOpacity(0.8)), + style: GoogleFonts.outfit( + fontSize: 12, + color: AppColors.warmGray.withValues(alpha: 0.8)), ), ], ), ), Container( decoration: BoxDecoration( - color: AppColors.warmCream.withOpacity(0.5), + color: AppColors.warmCream.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ IconButton( - icon: const Icon(Icons.remove, size: 20, color: AppColors.navyBlue), + icon: const Icon(Icons.remove, + size: 20, color: AppColors.navyBlue), onPressed: () { - if (_padInventoryCount > 0) setState(() => _padInventoryCount--); + if (_padInventoryCount > 0) { + setState(() => _padInventoryCount--); + } }, ), Text( '$_padInventoryCount', style: GoogleFonts.outfit( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.navyBlue - ), + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.navyBlue), ), IconButton( - icon: const Icon(Icons.add, size: 20, color: AppColors.navyBlue), + icon: const Icon(Icons.add, + size: 20, color: AppColors.navyBlue), onPressed: () => setState(() => _padInventoryCount++), ), ], @@ -289,7 +313,7 @@ class _PadSettingsDialogState extends ConsumerState { ), ], ), - + const SizedBox(height: 16), // Low Stock Threshold @@ -301,11 +325,15 @@ class _PadSettingsDialogState extends ConsumerState { children: [ Text( 'Low Stock Alert', - style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: AppColors.warmGray), + style: GoogleFonts.outfit( + fontWeight: FontWeight.w500, + color: AppColors.warmGray), ), Text( 'Warn when below $_lowInventoryThreshold', - style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray.withOpacity(0.8)), + style: GoogleFonts.outfit( + fontSize: 12, + color: AppColors.warmGray.withValues(alpha: 0.8)), ), ], ), @@ -318,7 +346,8 @@ class _PadSettingsDialogState extends ConsumerState { max: 20, divisions: 19, activeColor: AppColors.rose, - onChanged: (val) => setState(() => _lowInventoryThreshold = val.round()), + onChanged: (val) => + setState(() => _lowInventoryThreshold = val.round()), ), ), ], @@ -326,23 +355,26 @@ class _PadSettingsDialogState extends ConsumerState { // Auto Deduct Toggle SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text( - 'Auto-deduct on Log', - style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: AppColors.charcoal), - ), - subtitle: Text( - 'Reduce count when you log a pad', - style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), - ), - value: _isAutoInventoryEnabled, - onChanged: (val) => setState(() => _isAutoInventoryEnabled = val), - activeColor: AppColors.menstrualPhase, + contentPadding: EdgeInsets.zero, + title: Text( + 'Auto-deduct on Log', + style: GoogleFonts.outfit( + fontWeight: FontWeight.w500, color: AppColors.charcoal), + ), + subtitle: Text( + 'Reduce count when you log a pad', + style: GoogleFonts.outfit( + fontSize: 12, color: AppColors.warmGray), + ), + value: _isAutoInventoryEnabled, + onChanged: (val) => + setState(() => _isAutoInventoryEnabled = val), + activeThumbColor: AppColors.menstrualPhase, ), ], - + const SizedBox(height: 32), - + // Buttons Row( mainAxisAlignment: MainAxisAlignment.end, @@ -360,7 +392,8 @@ class _PadSettingsDialogState extends ConsumerState { style: ElevatedButton.styleFrom( backgroundColor: AppColors.navyBlue, foregroundColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), ), child: const Text('Save'), ), diff --git a/lib/widgets/pad_tracker_card.dart b/lib/widgets/pad_tracker_card.dart index 39e2e04..1e5c9d3 100644 --- a/lib/widgets/pad_tracker_card.dart +++ b/lib/widgets/pad_tracker_card.dart @@ -136,8 +136,9 @@ class _PadTrackerCardState extends ConsumerState { @override Widget build(BuildContext context) { final user = ref.watch(userProfileProvider); - if (user == null || !user.isPadTrackingEnabled) + if (user == null || !user.isPadTrackingEnabled) { return const SizedBox.shrink(); + } return GestureDetector( onTap: () { @@ -151,10 +152,10 @@ class _PadTrackerCardState extends ConsumerState { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), - border: Border.all(color: _statusColor.withOpacity(0.3)), + border: Border.all(color: _statusColor.withValues(alpha: 0.3)), boxShadow: [ BoxShadow( - color: _statusColor.withOpacity(0.1), + color: _statusColor.withValues(alpha: 0.1), blurRadius: 10, offset: const Offset(0, 4), ), @@ -167,7 +168,7 @@ class _PadTrackerCardState extends ConsumerState { Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: _statusColor.withOpacity(0.1), + color: _statusColor.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: @@ -224,7 +225,7 @@ class _PadTrackerCardState extends ConsumerState { borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: _progress, - backgroundColor: _statusColor.withOpacity(0.1), + backgroundColor: _statusColor.withValues(alpha: 0.1), valueColor: AlwaysStoppedAnimation(_statusColor), minHeight: 6, ), diff --git a/lib/widgets/quick_log_buttons.dart b/lib/widgets/quick_log_buttons.dart index 0a3b411..d0cfebd 100644 --- a/lib/widgets/quick_log_buttons.dart +++ b/lib/widgets/quick_log_buttons.dart @@ -3,8 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import '../providers/user_provider.dart'; import '../theme/app_theme.dart'; -import '../providers/navigation_provider.dart'; -import '../models/user_profile.dart'; import 'quick_log_dialog.dart'; class QuickLogButtons extends ConsumerWidget { @@ -92,8 +90,9 @@ class QuickLogButtons extends ConsumerWidget { }) { final isDark = Theme.of(context).brightness == Brightness.dark; - return Container( - width: 100, // Fixed width for grid item + return SizedBox( + height: 80, + width: 75, child: Material( color: Colors.transparent, child: InkWell( @@ -102,9 +101,11 @@ class QuickLogButtons extends ConsumerWidget { child: Container( padding: const EdgeInsets.symmetric(vertical: 16), decoration: BoxDecoration( - color: color.withOpacity(isDark ? 0.2 : 0.15), + color: color.withValues(alpha: 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.withValues(alpha: 0.3)) + : null, ), child: Column( mainAxisSize: MainAxisSize.min, @@ -117,7 +118,7 @@ class QuickLogButtons extends ConsumerWidget { style: GoogleFonts.outfit( fontSize: 12, // Slightly larger text fontWeight: FontWeight.w600, - color: isDark ? Colors.white.withOpacity(0.9) : color, + color: isDark ? Colors.white.withValues(alpha: 0.9) : color, ), ), ], diff --git a/lib/widgets/quick_log_dialog.dart b/lib/widgets/quick_log_dialog.dart index 22892c2..9e81a3f 100644 --- a/lib/widgets/quick_log_dialog.dart +++ b/lib/widgets/quick_log_dialog.dart @@ -7,7 +7,7 @@ import '../models/cycle_entry.dart'; import '../providers/user_provider.dart'; import '../providers/navigation_provider.dart'; import '../screens/log/pad_tracker_screen.dart'; -import '../theme/app_theme.dart'; +import '../services/notification_service.dart'; class QuickLogDialog extends ConsumerStatefulWidget { final String logType; @@ -39,7 +39,7 @@ class _QuickLogDialogState extends ConsumerState { }; final TextEditingController _cravingController = TextEditingController(); - List _cravings = []; + final List _cravings = []; List _recentCravings = []; @override @@ -110,7 +110,7 @@ class _QuickLogDialogState extends ConsumerState { } Widget _buildSymptomsLog() { - return Container( + return SizedBox( width: double.maxFinite, child: ListView( shrinkWrap: true, @@ -139,7 +139,7 @@ class _QuickLogDialogState extends ConsumerState { } Widget _buildCravingsLog() { - return Container( + return SizedBox( width: double.maxFinite, child: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/widgets/scripture_card.dart b/lib/widgets/scripture_card.dart index 15bb674..1275d6e 100644 --- a/lib/widgets/scripture_card.dart +++ b/lib/widgets/scripture_card.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; import '../theme/app_theme.dart'; import '../models/cycle_entry.dart'; @@ -35,11 +34,12 @@ class ScriptureCard extends StatelessWidget { borderRadius: BorderRadius.circular(20), border: Border.all( color: isDark - ? Colors.white.withOpacity(0.05) - : Colors.black.withOpacity(0.05)), + ? Colors.white.withValues(alpha: 0.05) + : Colors.black.withValues(alpha: 0.05)), boxShadow: [ BoxShadow( - color: _getPhaseColor(phase).withOpacity(isDark ? 0.05 : 0.15), + color: + _getPhaseColor(phase).withValues(alpha: isDark ? 0.05 : 0.15), blurRadius: 15, offset: const Offset(0, 8), ), @@ -60,7 +60,7 @@ class ScriptureCard extends StatelessWidget { height: 32, decoration: BoxDecoration( color: (isDark ? Colors.white : Colors.black) - .withOpacity(0.1), + .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( @@ -68,7 +68,7 @@ class ScriptureCard extends StatelessWidget { size: 18, color: isDark ? Colors.white70 - : AppColors.charcoal.withOpacity(0.8), + : AppColors.charcoal.withValues(alpha: 0.8), ), ), const SizedBox(width: 8), @@ -78,7 +78,7 @@ class ScriptureCard extends StatelessWidget { fontSize: 12, color: isDark ? const Color(0xFFE0E0E0) - : AppColors.charcoal.withOpacity(0.7), + : AppColors.charcoal.withValues(alpha: 0.7), letterSpacing: 0.5, ), ), @@ -111,11 +111,11 @@ class ScriptureCard extends StatelessWidget { horizontal: 10, vertical: 6), decoration: BoxDecoration( color: (isDark ? Colors.white : Colors.black) - .withOpacity(0.05), + .withValues(alpha: 0.05), borderRadius: BorderRadius.circular(8), border: Border.all( color: (isDark ? Colors.white : Colors.black) - .withOpacity(0.1), + .withValues(alpha: 0.1), ), ), child: Row( @@ -133,9 +133,8 @@ class ScriptureCard extends StatelessWidget { Icon( Icons.swap_horiz, size: 14, - color: isDark - ? Colors.white38 - : AppColors.warmGray, + color: + isDark ? Colors.white38 : AppColors.warmGray, ), ], ), @@ -157,22 +156,22 @@ class ScriptureCard extends StatelessWidget { switch (phase) { case CyclePhase.menstrual: return [ - AppColors.menstrualPhase.withOpacity(isDark ? 0.15 : 0.6), + AppColors.menstrualPhase.withValues(alpha: isDark ? 0.15 : 0.6), baseColor, ]; case CyclePhase.follicular: return [ - AppColors.follicularPhase.withOpacity(isDark ? 0.15 : 0.3), + AppColors.follicularPhase.withValues(alpha: isDark ? 0.15 : 0.3), baseColor, ]; case CyclePhase.ovulation: return [ - AppColors.ovulationPhase.withOpacity(isDark ? 0.15 : 0.5), + AppColors.ovulationPhase.withValues(alpha: isDark ? 0.15 : 0.5), baseColor, ]; case CyclePhase.luteal: return [ - AppColors.lutealPhase.withOpacity(isDark ? 0.15 : 0.3), + AppColors.lutealPhase.withValues(alpha: isDark ? 0.15 : 0.3), baseColor, ]; } diff --git a/lib/widgets/tip_card.dart b/lib/widgets/tip_card.dart index 7d1554e..73c8b11 100644 --- a/lib/widgets/tip_card.dart +++ b/lib/widgets/tip_card.dart @@ -25,12 +25,13 @@ class TipCard extends StatelessWidget { decoration: BoxDecoration( 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.withValues(alpha: 0.05)) + : null, boxShadow: [ BoxShadow( color: (isDark ? Colors.black : AppColors.charcoal) - .withOpacity(0.05), + .withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -43,7 +44,7 @@ class TipCard extends StatelessWidget { width: 40, height: 40, decoration: BoxDecoration( - color: AppColors.sageGreen.withOpacity(isDark ? 0.2 : 0.15), + color: AppColors.sageGreen.withValues(alpha: isDark ? 0.2 : 0.15), borderRadius: BorderRadius.circular(10), ), child: const Icon( diff --git a/pubspec.lock b/pubspec.lock index 888dd70..c9fcd0a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -178,7 +178,7 @@ packages: source: hosted version: "4.10.1" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" @@ -705,7 +705,7 @@ packages: source: hosted version: "2.2.1" path_provider_platform_interface: - dependency: transitive + dependency: "direct main" description: name: path_provider_platform_interface sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" @@ -1022,7 +1022,7 @@ packages: source: hosted version: "0.7.7" timezone: - dependency: transitive + dependency: "direct main" description: name: timezone sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" diff --git a/pubspec.yaml b/pubspec.yaml index aff2430..9780ac1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,9 @@ dependencies: icalendar_parser: ^2.0.0 # For .ics file generation share_plus: ^7.2.2 # For sharing files local_auth: ^3.0.0 + collection: ^1.18.0 + timezone: ^0.9.4 + path_provider_platform_interface: ^2.1.2 dev_dependencies: flutter_test: diff --git a/test/scripture_provider_test.dart b/test/scripture_provider_test.dart index 6821829..b53b130 100644 --- a/test/scripture_provider_test.dart +++ b/test/scripture_provider_test.dart @@ -1,7 +1,6 @@ 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'; @@ -12,7 +11,8 @@ 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, int index) + getScriptureForPhaseByIndexFn; final Scripture? Function(String phase)? getRandomScriptureForPhaseFn; FakeScriptureDatabase({ @@ -22,7 +22,8 @@ class FakeScriptureDatabase implements ScriptureDatabase { }); @override - int getScriptureCountForPhase(String phase) => getScriptureCountForPhaseFn(phase); + int getScriptureCountForPhase(String phase) => + getScriptureCountForPhaseFn(phase); @override Scripture? getScriptureForPhaseByIndex(String phase, int index) => @@ -30,7 +31,9 @@ class FakeScriptureDatabase implements ScriptureDatabase { @override Scripture? getRandomScriptureForPhase(String phase) => - getRandomScriptureForPhaseFn != null ? getRandomScriptureForPhaseFn!(phase) : null; + getRandomScriptureForPhaseFn != null + ? getRandomScriptureForPhaseFn!(phase) + : null; // Unimplemented methods (not used by ScriptureNotifier) @override @@ -40,10 +43,12 @@ class FakeScriptureDatabase implements ScriptureDatabase { Scripture getHusbandScripture() => throw UnimplementedError(); @override - Future loadScriptures() => Future.value(); // Can be mocked to do nothing + Future loadScriptures() => + Future.value(); // Can be mocked to do nothing @override - Scripture? getRecommendedScripture(CycleEntry entry) => throw UnimplementedError(); + Scripture? getRecommendedScripture(CycleEntry entry) => + throw UnimplementedError(); @override Scripture getScriptureForPhase(String phase) => throw UnimplementedError(); @@ -55,7 +60,7 @@ void main() { late String testPath; setUpAll(() async { - testPath = Directory.current.path + '/test_hive_temp_scripture_provider'; + testPath = '${Directory.current.path}/test_hive_temp_scripture_provider'; final Directory tempDir = Directory(testPath); if (!await tempDir.exists()) { await tempDir.create(recursive: true); @@ -142,22 +147,25 @@ void main() { // 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); - + 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).currentScripture, testScripture2); expect(container.read(scriptureProvider).currentIndex, 1); // Second next notifier.getNextScripture(); - expect(container.read(scriptureProvider).currentScripture, testScripture3); + 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).currentScripture, testScripture1); expect(container.read(scriptureProvider).currentIndex, 0); }); @@ -181,22 +189,25 @@ void main() { // 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); - + 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).currentScripture, testScripture3); expect(container.read(scriptureProvider).currentIndex, 2); // Second previous notifier.getPreviousScripture(); - expect(container.read(scriptureProvider).currentScripture, testScripture2); + 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).currentScripture, testScripture1); expect(container.read(scriptureProvider).currentIndex, 0); }); @@ -204,7 +215,8 @@ void main() { 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 + getScriptureForPhaseByIndexFn: (phase, index) => scriptures[ + index % scriptures.length], // Ensure it always returns a valid one ); container = ProviderContainer( @@ -224,7 +236,8 @@ void main() { final state = container.read(scriptureProvider); expect(state.currentScripture, isNotNull); - expect(state.currentScripture, isIn(scriptures)); // Ensure it's one of the valid scriptures + expect(state.currentScripture, + isIn(scriptures)); // Ensure it's one of the valid scriptures expect(state.currentIndex, isNonNegative); expect(state.currentIndex, lessThan(scriptures.length)); }); @@ -259,4 +272,4 @@ void main() { expect(container.read(scriptureProvider), initialState); }); }); -} \ No newline at end of file +} diff --git a/test/scripture_test.dart b/test/scripture_test.dart index ad86679..1136963 100644 --- a/test/scripture_test.dart +++ b/test/scripture_test.dart @@ -7,7 +7,6 @@ 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 @@ -36,7 +35,7 @@ void main() { // 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'; + testPath = '${Directory.current.path}/test_hive_temp'; // Ensure the directory exists final Directory tempDir = Directory(testPath); @@ -46,7 +45,8 @@ void main() { // Create and configure the mock BibleXmlParser final mockBibleXmlParser = MockBibleXmlParser(); - when(mockBibleXmlParser.getVerseFromAsset(any, any)).thenAnswer((invocation) async { + 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'; @@ -58,7 +58,8 @@ void main() { 'flutter/assets', (ByteData? message) async { final String key = utf8.decode(message!.buffer.asUint8List()); - if (key == 'assets/scriptures.json' || key == 'assets/scriptures_optimized.json') { + if (key == 'assets/scriptures.json' || + key == 'assets/scriptures_optimized.json') { final json = { "menstrual": [ { @@ -120,8 +121,10 @@ void main() { Hive.init(testPath); Hive.registerAdapter(ScriptureAdapter()); // Register your adapter - Hive.registerAdapter(BibleTranslationAdapter()); // Register BibleTranslationAdapter - database = ScriptureDatabase(bibleXmlParser: mockBibleXmlParser); // Instantiate with mock + Hive.registerAdapter( + BibleTranslationAdapter()); // Register BibleTranslationAdapter + database = ScriptureDatabase( + bibleXmlParser: mockBibleXmlParser); // Instantiate with mock await database.loadScriptures(); }); @@ -145,11 +148,11 @@ void main() { class _MockPathProviderPlatform extends PathProviderPlatform { @override Future getApplicationSupportPath() async { - return Directory.current.path + '/test_hive_temp'; + return '${Directory.current.path}/test_hive_temp'; } @override Future getApplicationDocumentsPath() async { - return Directory.current.path + '/test_hive_temp'; + return '${Directory.current.path}/test_hive_temp'; } } diff --git a/test/widget_test.dart b/test/widget_test.dart index a0a6698..cd77878 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,6 +1,4 @@ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:christian_period_tracker/main.dart'; void main() { testWidgets('App loads correctly', (WidgetTester tester) async { diff --git a/tool/optimize_assets.dart b/tool/optimize_assets.dart index 637456a..26c2b27 100644 --- a/tool/optimize_assets.dart +++ b/tool/optimize_assets.dart @@ -1,26 +1,76 @@ import 'dart:convert'; import 'dart:io'; import 'package:xml/xml.dart'; +import 'package:flutter/foundation.dart'; // Copy of book abbreviations from BibleXmlParser const Map _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', + '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 @@ -35,12 +85,12 @@ final Map _translationFiles = { }; void main() async { - print('Starting asset optimization...'); + debugPrint('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.'); + debugPrint('Error: assets/scriptures.json not found.'); return; } final Map data = json.decode(jsonFile.readAsStringSync()); @@ -52,14 +102,14 @@ void main() async { final path = entry.value; final file = File(path); if (file.existsSync()) { - print('Parsing $key from $path...'); + debugPrint('Parsing $key from $path...'); try { xmlDocs[key] = XmlDocument.parse(file.readAsStringSync()); } catch (e) { - print('Error parsing $path: $e'); + debugPrint('Error parsing $path: $e'); } } else { - print('Warning: $path not found.'); + debugPrint('Warning: $path not found.'); } } @@ -90,7 +140,8 @@ void main() async { outputFile.writeAsStringSync(json.encode(data)); // Minified // outputFile.writeAsStringSync(const JsonEncoder.withIndent(' ').convert(data)); // Pretty print - print('Optimization complete. Wrote to assets/scriptures_optimized.json'); + debugPrint( + 'Optimization complete. Wrote to assets/scriptures_optimized.json'); } Future _processList(List list, Map xmlDocs) async { @@ -101,31 +152,27 @@ Future _processList(List list, Map xmlDocs) async { // Parse reference final parts = _parseReference(reference); if (parts == null) { - print('Skipping invalid reference: $reference'); + debugPrint('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. + + // 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']!) - ); - + 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'); + debugPrint('Warning: Could not find $reference in $transKey'); } } } @@ -140,92 +187,96 @@ Map? _parseReference(String reference) { 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 + 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], + // 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; +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; + // 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( + // -- 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('bname'); + final nameAttr = element.getAttribute('n'); return nameAttr?.toLowerCase() == lookupBookName.toLowerCase() || - nameAttr?.toLowerCase() == bookName.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(); } + + 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(); +} diff --git a/web/index.html b/web/index.html index ddc7c27..0249a61 100644 --- a/web/index.html +++ b/web/index.html @@ -70,7 +70,7 @@
- + \ No newline at end of file