This commit is contained in:
2025-12-30 23:18:09 -06:00
173 changed files with 251757 additions and 0 deletions

43
.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

45
.metadata Normal file
View File

@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: android
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: ios
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: linux
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: macos
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: web
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: windows
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

127
README.md Normal file
View File

@@ -0,0 +1,127 @@
# Christian Period Tracker
A faith-centered period and fertility tracking app for Christian women and their husbands.
## Features
### Wife's App (Primary)
- **Cycle Tracking** - Period logging, predictions, and phase identification
- **Symptom Logging** - Mood, energy, cramps, and more
- **Daily Devotionals** - Phase-specific scripture and reflections
- **NFP Support** - BBT, cervical mucus tracking (for married users)
- **Calendar View** - Color-coded cycle visualization
- **Private Data** - All data stored locally on device
### Husband's Companion App
- **Cycle Overview** - See where she is in her cycle
- **Support Tips** - Phase-specific ways to help
- **Scripture** - Verses for loving husbands
- **Learning Library** - Understanding her cycle, biblical manhood
## Marital Status Awareness
The app adapts content based on relationship status:
- **Single/Engaged**: Health tracking only, no intimacy/fertility content
- **Married**: Full features including NFP, fertility window, partner sharing
## Tech Stack
- **Framework**: Flutter 3.24+
- **State Management**: Riverpod
- **Local Database**: Hive (privacy-first, no cloud required)
- **UI**: Material 3 + Custom Design System
- **Fonts**: Outfit (UI), Lora (Scripture)
## Getting Started
### Prerequisites
- Flutter SDK 3.24+
- Dart 3.5+
### Installation
```bash
# Clone the repository
cd christian_period_tracker
# Install dependencies
flutter pub get
# Generate Hive adapters (if needed)
dart run build_runner build --delete-conflicting-outputs
# Run the app
flutter run
```
### Platforms
- ✅ iOS (iPhone + iPad)
- ✅ Android (Phone + Tablet)
- ✅ Web (Progressive Web App)
## Project Structure
```
lib/
├── main.dart # App entry point
├── theme/
│ └── app_theme.dart # Color palette, typography
├── models/
│ ├── user_profile.dart # User & relationship status
│ ├── cycle_entry.dart # Daily tracking data
│ └── scripture.dart # Scripture database
├── screens/
│ ├── splash_screen.dart # Animated splash
│ ├── onboarding/ # Welcome, name, status, cycle setup
│ ├── home/ # Dashboard with cycle ring
│ ├── calendar/ # Month view with phase markers
│ ├── log/ # Symptom & period logging
│ ├── devotional/ # Daily scripture & reflection
│ └── husband/ # Husband companion screens
└── widgets/
├── cycle_ring.dart # Animated progress ring
├── scripture_card.dart # Phase-colored verse display
├── quick_log_buttons.dart # Quick action buttons
└── tip_card.dart # Contextual tips
```
## Color Palettes
### Wife's App
| Color | Hex | Usage |
|-------|-----|-------|
| Blush Pink | `#F8E1E7` | Backgrounds, accents |
| Rose | `#E8A0B0` | Secondary actions |
| Sage Green | `#A8C5A8` | Primary, CTAs |
| Lavender | `#D4C4E8` | Tertiary, calm states |
| Cream | `#FDF8F5` | Scaffold background |
### Husband's App
| Color | Hex | Usage |
|-------|-----|-------|
| Navy Blue | `#2C3E50` | Primary |
| Steel Blue | `#5D7B93` | Secondary |
| Warm Cream | `#F5F0E8` | Background |
| Gold | `#C9A961` | Accents, scripture |
## Privacy
- **Local-first**: All data stored on device using Hive
- **No account required**: Works fully offline
- **Optional cloud sync**: Future feature with end-to-end encryption
- **Biometric lock**: Planned for sensitive data
## License
MIT
## Contributing
Contributions welcome! Please read the contribution guidelines first.

28
analysis_options.yaml Normal file
View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

44
android/app/build.gradle Normal file
View File

@@ -0,0 +1,44 @@
plugins {
id "com.android.application"
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
android {
namespace = "com.faithapps.christian_period_tracker"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.faithapps.christian_period_tracker"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="christian_period_tracker"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.faithapps.christian_period_tracker
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

18
android/build.gradle Normal file
View File

@@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,14 @@
<<<<<<< HEAD
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
=======
#Fri Dec 19 21:26:00 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
>>>>>>> 6742220 (Your commit message here)

25
android/settings.gradle Normal file
View File

@@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}
include ":app"

33616
assets/bible_xml/ESV.xml Normal file

File diff suppressed because it is too large Load Diff

33766
assets/bible_xml/KJV.xml Normal file

File diff suppressed because it is too large Load Diff

33615
assets/bible_xml/MSG.xml Normal file

File diff suppressed because it is too large Load Diff

33614
assets/bible_xml/NASB.xml Normal file

File diff suppressed because it is too large Load Diff

33615
assets/bible_xml/NIV.xml Normal file

File diff suppressed because it is too large Load Diff

33615
assets/bible_xml/NKJV.xml Normal file

File diff suppressed because it is too large Load Diff

33617
assets/bible_xml/NLT.xml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
placeholder

208
assets/scriptures.json Normal file
View File

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

File diff suppressed because one or more lines are too long

34
ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.faithapps.christianPeriodTracker;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.faithapps.christianPeriodTracker.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.faithapps.christianPeriodTracker.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.faithapps.christianPeriodTracker.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.faithapps.christianPeriodTracker;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.faithapps.christianPeriodTracker;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

49
ios/Runner/Info.plist Normal file
View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Christian Period Tracker</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>christian_period_tracker</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

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

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

55
lib/main.dart Normal file
View File

@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:google_fonts/google_fonts.dart';
import 'models/scripture.dart';
import 'theme/app_theme.dart';
import 'screens/splash_screen.dart';
import 'models/user_profile.dart';
import 'models/cycle_entry.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Hive for local storage
await Hive.initFlutter();
// Register Hive adapters
Hive.registerAdapter(UserProfileAdapter());
Hive.registerAdapter(CycleEntryAdapter());
Hive.registerAdapter(RelationshipStatusAdapter());
Hive.registerAdapter(FertilityGoalAdapter());
Hive.registerAdapter(MoodLevelAdapter());
Hive.registerAdapter(FlowIntensityAdapter());
Hive.registerAdapter(CervicalMucusTypeAdapter());
Hive.registerAdapter(CyclePhaseAdapter());
Hive.registerAdapter(UserRoleAdapter());
Hive.registerAdapter(BibleTranslationAdapter());
Hive.registerAdapter(ScriptureAdapter()); // Register Scripture adapter
// Open boxes and load scriptures in parallel
await Future.wait([
Hive.openBox<UserProfile>('user_profile'),
Hive.openBox<CycleEntry>('cycle_entries'),
ScriptureDatabase().loadScriptures(),
]);
runApp(const ProviderScope(child: ChristianPeriodTrackerApp()));
}
class ChristianPeriodTrackerApp extends StatelessWidget {
const ChristianPeriodTrackerApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Christian Period Tracker',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
home: const SplashScreen(),
);
}
}

421
lib/models/cycle_entry.dart Normal file
View File

@@ -0,0 +1,421 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import '../theme/app_theme.dart';
part 'cycle_entry.g.dart';
/// Mood levels for tracking
@HiveType(typeId: 3)
enum MoodLevel {
@HiveField(0)
verySad,
@HiveField(1)
sad,
@HiveField(2)
neutral,
@HiveField(3)
happy,
@HiveField(4)
veryHappy,
}
/// Flow intensity for period days
@HiveType(typeId: 4)
enum FlowIntensity {
@HiveField(0)
spotting,
@HiveField(1)
light,
@HiveField(2)
medium,
@HiveField(3)
heavy,
}
/// Cervical mucus type for NFP tracking
@HiveType(typeId: 5)
enum CervicalMucusType {
@HiveField(0)
dry,
@HiveField(1)
sticky,
@HiveField(2)
creamy,
@HiveField(3)
eggWhite,
@HiveField(4)
watery,
}
/// Cycle phase
@HiveType(typeId: 6)
enum CyclePhase {
@HiveField(0)
menstrual,
@HiveField(1)
follicular,
@HiveField(2)
ovulation,
@HiveField(3)
luteal,
}
/// Daily cycle entry
@HiveType(typeId: 7)
class CycleEntry extends HiveObject {
@HiveField(0)
String id;
@HiveField(1)
DateTime date;
@HiveField(2)
bool isPeriodDay;
@HiveField(3)
FlowIntensity? flowIntensity;
@HiveField(4)
MoodLevel? mood;
@HiveField(5)
int? energyLevel; // 1-5
@HiveField(6)
int? crampIntensity; // 1-5
@HiveField(7)
bool hasHeadache;
@HiveField(8)
bool hasBloating;
@HiveField(9)
bool hasBreastTenderness;
@HiveField(10)
bool hasFatigue;
@HiveField(11)
bool hasAcne;
@HiveField(22)
bool hasLowerBackPain;
@HiveField(23)
bool hasConstipation;
@HiveField(24)
bool hasDiarrhea;
@HiveField(25)
int? stressLevel; // 1-5
@HiveField(26)
bool hasInsomnia;
@HiveField(12)
double? basalBodyTemperature; // in Fahrenheit
@HiveField(13)
CervicalMucusType? cervicalMucus;
@HiveField(14)
bool? ovulationTestPositive;
@HiveField(15)
String? notes;
@HiveField(16)
int? sleepHours;
@HiveField(17)
int? waterIntake; // glasses
@HiveField(18)
bool hadExercise;
@HiveField(19)
bool hadIntimacy; // For married users only
@HiveField(20)
DateTime createdAt;
@HiveField(21)
DateTime updatedAt;
@HiveField(27)
List<String>? cravings;
@HiveField(28)
String? husbandNotes; // Separate notes for husband
@HiveField(29)
bool? intimacyProtected; // null = no intimacy, true = protected, false = unprotected
CycleEntry({
required this.id,
required this.date,
this.isPeriodDay = false,
this.flowIntensity,
this.mood,
this.energyLevel,
this.crampIntensity,
this.hasHeadache = false,
this.hasBloating = false,
this.hasBreastTenderness = false,
this.hasFatigue = false,
this.hasAcne = false,
this.hasLowerBackPain = false,
this.hasConstipation = false,
this.hasDiarrhea = false,
this.stressLevel,
this.hasInsomnia = false,
this.basalBodyTemperature,
this.cervicalMucus,
this.ovulationTestPositive,
this.notes,
this.cravings,
this.sleepHours,
this.waterIntake,
this.hadExercise = false,
this.hadIntimacy = false,
this.intimacyProtected,
required this.createdAt,
required this.updatedAt,
this.husbandNotes,
});
List<bool> get _symptomsList => [
hasHeadache,
hasBloating,
hasBreastTenderness,
hasFatigue,
hasAcne,
hasLowerBackPain,
hasConstipation,
hasDiarrhea,
hasInsomnia,
(crampIntensity != null && crampIntensity! > 0),
(stressLevel != null && stressLevel! > 1),
];
/// Check if any symptoms are logged
bool get hasSymptoms => _symptomsList.contains(true);
/// Check if NFP data is logged
bool get hasNFPData =>
basalBodyTemperature != null ||
cervicalMucus != null ||
ovulationTestPositive != null;
/// Get symptom count
int get symptomCount => _symptomsList.where((s) => s).length;
// ... (omitted getters)
/// Copy with updated fields
CycleEntry copyWith({
String? id,
DateTime? date,
bool? isPeriodDay,
FlowIntensity? flowIntensity,
MoodLevel? mood,
int? energyLevel,
int? crampIntensity,
bool? hasHeadache,
bool? hasBloating,
bool? hasBreastTenderness,
bool? hasFatigue,
bool? hasAcne,
bool? hasLowerBackPain,
bool? hasConstipation,
bool? hasDiarrhea,
int? stressLevel,
bool? hasInsomnia,
double? basalBodyTemperature,
CervicalMucusType? cervicalMucus,
bool? ovulationTestPositive,
String? notes,
List<String>? cravings,
int? sleepHours,
int? waterIntake,
bool? hadExercise,
bool? hadIntimacy,
bool? intimacyProtected,
DateTime? createdAt,
DateTime? updatedAt,
String? husbandNotes,
}) {
return CycleEntry(
id: id ?? this.id,
date: date ?? this.date,
isPeriodDay: isPeriodDay ?? this.isPeriodDay,
flowIntensity: flowIntensity ?? this.flowIntensity,
mood: mood ?? this.mood,
energyLevel: energyLevel ?? this.energyLevel,
crampIntensity: crampIntensity ?? this.crampIntensity,
hasHeadache: hasHeadache ?? this.hasHeadache,
hasBloating: hasBloating ?? this.hasBloating,
hasBreastTenderness: hasBreastTenderness ?? this.hasBreastTenderness,
hasFatigue: hasFatigue ?? this.hasFatigue,
hasAcne: hasAcne ?? this.hasAcne,
hasLowerBackPain: hasLowerBackPain ?? this.hasLowerBackPain,
hasConstipation: hasConstipation ?? this.hasConstipation,
hasDiarrhea: hasDiarrhea ?? this.hasDiarrhea,
stressLevel: stressLevel ?? this.stressLevel,
hasInsomnia: hasInsomnia ?? this.hasInsomnia,
basalBodyTemperature: basalBodyTemperature ?? this.basalBodyTemperature,
cervicalMucus: cervicalMucus ?? this.cervicalMucus,
ovulationTestPositive: ovulationTestPositive ?? this.ovulationTestPositive,
notes: notes ?? this.notes,
cravings: cravings ?? this.cravings,
sleepHours: sleepHours ?? this.sleepHours,
waterIntake: waterIntake ?? this.waterIntake,
hadExercise: hadExercise ?? this.hadExercise,
hadIntimacy: hadIntimacy ?? this.hadIntimacy,
intimacyProtected: intimacyProtected ?? this.intimacyProtected,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? DateTime.now(),
husbandNotes: husbandNotes ?? this.husbandNotes,
);
}
}
/// Extension to get display string for enums
extension MoodLevelExtension on MoodLevel {
String get emoji {
switch (this) {
case MoodLevel.verySad:
return '😢';
case MoodLevel.sad:
return '😕';
case MoodLevel.neutral:
return '😐';
case MoodLevel.happy:
return '🙂';
case MoodLevel.veryHappy:
return '😄';
}
}
String get label {
switch (this) {
case MoodLevel.verySad:
return 'Very Sad';
case MoodLevel.sad:
return 'Sad';
case MoodLevel.neutral:
return 'Neutral';
case MoodLevel.happy:
return 'Happy';
case MoodLevel.veryHappy:
return 'Very Happy';
}
}
}
extension FlowIntensityExtension on FlowIntensity {
String get label {
switch (this) {
case FlowIntensity.spotting:
return 'Spotting';
case FlowIntensity.light:
return 'Light';
case FlowIntensity.medium:
return 'Medium';
case FlowIntensity.heavy:
return 'Heavy';
}
}
}
extension CyclePhaseExtension on CyclePhase {
String get label {
switch (this) {
case CyclePhase.menstrual:
return 'Menstrual';
case CyclePhase.follicular:
return 'Follicular';
case CyclePhase.ovulation:
return 'Ovulation';
case CyclePhase.luteal:
return 'Luteal';
}
}
String get emoji {
switch (this) {
case CyclePhase.menstrual:
return '🩸';
case CyclePhase.follicular:
return '🌱';
case CyclePhase.ovulation:
return '🌸';
case CyclePhase.luteal:
return '🌙';
}
}
Color get color {
switch (this) {
case CyclePhase.menstrual:
return AppColors.menstrualPhase;
case CyclePhase.follicular:
return AppColors.follicularPhase;
case CyclePhase.ovulation:
return AppColors.ovulationPhase;
case CyclePhase.luteal:
return AppColors.lutealPhase;
}
}
List<Color> get gradientColors {
switch (this) {
case CyclePhase.menstrual:
return [AppColors.rose, AppColors.menstrualPhase, AppColors.blushPink];
case CyclePhase.follicular:
return [
AppColors.sageGreen,
AppColors.follicularPhase,
AppColors.sageGreen.withOpacity(0.7)
];
case CyclePhase.ovulation:
return [AppColors.lavender, AppColors.ovulationPhase, AppColors.rose];
case CyclePhase.luteal:
return [
AppColors.lutealPhase,
AppColors.lavender,
AppColors.blushPink
];
}
}
String get description {
switch (this) {
case CyclePhase.menstrual:
return 'A time for rest and reflection';
case CyclePhase.follicular:
return 'A time of renewal and energy';
case CyclePhase.ovulation:
return 'Peak fertility window';
case CyclePhase.luteal:
return 'A time for patience and self-care';
}
}
}

View File

@@ -0,0 +1,334 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'cycle_entry.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class CycleEntryAdapter extends TypeAdapter<CycleEntry> {
@override
final int typeId = 7;
@override
CycleEntry read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return CycleEntry(
id: fields[0] as String,
date: fields[1] as DateTime,
isPeriodDay: fields[2] as bool,
flowIntensity: fields[3] as FlowIntensity?,
mood: fields[4] as MoodLevel?,
energyLevel: fields[5] as int?,
crampIntensity: fields[6] as int?,
hasHeadache: fields[7] as bool,
hasBloating: fields[8] as bool,
hasBreastTenderness: fields[9] as bool,
hasFatigue: fields[10] as bool,
hasAcne: fields[11] as bool,
hasLowerBackPain: fields[22] as bool,
hasConstipation: fields[23] as bool,
hasDiarrhea: fields[24] as bool,
stressLevel: fields[25] as int?,
hasInsomnia: fields[26] as bool,
basalBodyTemperature: fields[12] as double?,
cervicalMucus: fields[13] as CervicalMucusType?,
ovulationTestPositive: fields[14] as bool?,
notes: fields[15] as String?,
cravings: (fields[27] as List?)?.cast<String>(),
sleepHours: fields[16] as int?,
waterIntake: fields[17] as int?,
hadExercise: fields[18] as bool,
hadIntimacy: fields[19] as bool,
intimacyProtected: fields[29] as bool?,
createdAt: fields[20] as DateTime,
updatedAt: fields[21] as DateTime,
husbandNotes: fields[28] as String?,
);
}
@override
void write(BinaryWriter writer, CycleEntry obj) {
writer
..writeByte(30)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.date)
..writeByte(2)
..write(obj.isPeriodDay)
..writeByte(3)
..write(obj.flowIntensity)
..writeByte(4)
..write(obj.mood)
..writeByte(5)
..write(obj.energyLevel)
..writeByte(6)
..write(obj.crampIntensity)
..writeByte(7)
..write(obj.hasHeadache)
..writeByte(8)
..write(obj.hasBloating)
..writeByte(9)
..write(obj.hasBreastTenderness)
..writeByte(10)
..write(obj.hasFatigue)
..writeByte(11)
..write(obj.hasAcne)
..writeByte(22)
..write(obj.hasLowerBackPain)
..writeByte(23)
..write(obj.hasConstipation)
..writeByte(24)
..write(obj.hasDiarrhea)
..writeByte(25)
..write(obj.stressLevel)
..writeByte(26)
..write(obj.hasInsomnia)
..writeByte(12)
..write(obj.basalBodyTemperature)
..writeByte(13)
..write(obj.cervicalMucus)
..writeByte(14)
..write(obj.ovulationTestPositive)
..writeByte(15)
..write(obj.notes)
..writeByte(16)
..write(obj.sleepHours)
..writeByte(17)
..write(obj.waterIntake)
..writeByte(18)
..write(obj.hadExercise)
..writeByte(19)
..write(obj.hadIntimacy)
..writeByte(20)
..write(obj.createdAt)
..writeByte(21)
..write(obj.updatedAt)
..writeByte(27)
..write(obj.cravings)
..writeByte(28)
..write(obj.husbandNotes)
..writeByte(29)
..write(obj.intimacyProtected);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CycleEntryAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class MoodLevelAdapter extends TypeAdapter<MoodLevel> {
@override
final int typeId = 3;
@override
MoodLevel read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return MoodLevel.verySad;
case 1:
return MoodLevel.sad;
case 2:
return MoodLevel.neutral;
case 3:
return MoodLevel.happy;
case 4:
return MoodLevel.veryHappy;
default:
return MoodLevel.verySad;
}
}
@override
void write(BinaryWriter writer, MoodLevel obj) {
switch (obj) {
case MoodLevel.verySad:
writer.writeByte(0);
break;
case MoodLevel.sad:
writer.writeByte(1);
break;
case MoodLevel.neutral:
writer.writeByte(2);
break;
case MoodLevel.happy:
writer.writeByte(3);
break;
case MoodLevel.veryHappy:
writer.writeByte(4);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MoodLevelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class FlowIntensityAdapter extends TypeAdapter<FlowIntensity> {
@override
final int typeId = 4;
@override
FlowIntensity read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return FlowIntensity.spotting;
case 1:
return FlowIntensity.light;
case 2:
return FlowIntensity.medium;
case 3:
return FlowIntensity.heavy;
default:
return FlowIntensity.spotting;
}
}
@override
void write(BinaryWriter writer, FlowIntensity obj) {
switch (obj) {
case FlowIntensity.spotting:
writer.writeByte(0);
break;
case FlowIntensity.light:
writer.writeByte(1);
break;
case FlowIntensity.medium:
writer.writeByte(2);
break;
case FlowIntensity.heavy:
writer.writeByte(3);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FlowIntensityAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class CervicalMucusTypeAdapter extends TypeAdapter<CervicalMucusType> {
@override
final int typeId = 5;
@override
CervicalMucusType read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return CervicalMucusType.dry;
case 1:
return CervicalMucusType.sticky;
case 2:
return CervicalMucusType.creamy;
case 3:
return CervicalMucusType.eggWhite;
case 4:
return CervicalMucusType.watery;
default:
return CervicalMucusType.dry;
}
}
@override
void write(BinaryWriter writer, CervicalMucusType obj) {
switch (obj) {
case CervicalMucusType.dry:
writer.writeByte(0);
break;
case CervicalMucusType.sticky:
writer.writeByte(1);
break;
case CervicalMucusType.creamy:
writer.writeByte(2);
break;
case CervicalMucusType.eggWhite:
writer.writeByte(3);
break;
case CervicalMucusType.watery:
writer.writeByte(4);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CervicalMucusTypeAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class CyclePhaseAdapter extends TypeAdapter<CyclePhase> {
@override
final int typeId = 6;
@override
CyclePhase read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return CyclePhase.menstrual;
case 1:
return CyclePhase.follicular;
case 2:
return CyclePhase.ovulation;
case 3:
return CyclePhase.luteal;
default:
return CyclePhase.menstrual;
}
}
@override
void write(BinaryWriter writer, CyclePhase obj) {
switch (obj) {
case CyclePhase.menstrual:
writer.writeByte(0);
break;
case CyclePhase.follicular:
writer.writeByte(1);
break;
case CyclePhase.ovulation:
writer.writeByte(2);
break;
case CyclePhase.luteal:
writer.writeByte(3);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CyclePhaseAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

541
lib/models/scripture.dart Normal file
View File

@@ -0,0 +1,541 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/services.dart';
import 'package:hive_flutter/hive_flutter.dart'; // Import Hive
import '../services/bible_xml_parser.dart'; // Import the XML parser
import 'cycle_entry.dart';
import 'user_profile.dart';
part 'scripture.g.dart'; // Hive generated adapter
/// Scripture model for daily verses and devotionals
@HiveType(typeId: 10) // Unique typeId for Scripture
class Scripture extends HiveObject {
@HiveField(0)
final Map<BibleTranslation, String> verses;
@HiveField(1)
final String reference;
@HiveField(2)
final String? reflection;
@HiveField(3)
final List<String> applicablePhases;
@HiveField(4)
final List<String> applicableContexts;
Scripture({
required this.verses,
required this.reference,
this.reflection,
this.applicablePhases = const [],
this.applicableContexts = const [],
});
factory Scripture.fromJson(Map<String, dynamic> json) {
return Scripture(
verses: (json['verses'] as Map<String, dynamic>).map((key, value) =>
MapEntry(
BibleTranslation.values.firstWhere((e) => e.name == key), value)),
reference: json['reference'],
reflection: json['reflection'],
applicablePhases: (json['applicablePhases'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
applicableContexts: (json['applicableContexts'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
);
}
String getVerse(BibleTranslation translation) {
return verses[translation] ??
verses[BibleTranslation.esv] ??
verses.values.first;
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is Scripture &&
runtimeType == other.runtimeType &&
reference == other.reference &&
reflection == other.reflection &&
_listEquals(applicablePhases, other.applicablePhases) &&
_listEquals(applicableContexts, other.applicableContexts) &&
_mapEquals(verses, other.verses));
@override
int get hashCode =>
reference.hashCode ^
reflection.hashCode ^
Object.hashAll(applicablePhases) ^
Object.hashAll(applicableContexts) ^
Object.hashAll(verses.entries) ^
reflection.hashCode;
// Helper for list equality check
static bool _listEquals<T>(List<T>? a, List<T>? b) {
if (a == null) return b == null;
if (b == null) return false;
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
// Helper for map equality check
static bool _mapEquals<K, V>(Map<K, V>? a, Map<K, V>? b) {
if (a == null) return b == null;
if (b == null) return false;
if (a.length != b.length) return false;
if (a.keys.length != b.keys.length) return false; // Added length check
for (final key in a.keys) {
if (!b.containsKey(key) || a[key] != b[key]) return false; // Added containsKey check
}
return true;
}
}
/// Pre-defined scriptures for the app
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<Scripture> _scriptureBox;
// Mapping of BibleTranslation to its XML asset path
final Map<BibleTranslation, String> _translationFileMapping = {
BibleTranslation.esv: 'bible_xml/ESV.xml',
BibleTranslation.niv: 'bible_xml/NIV.xml',
BibleTranslation.nkjv: 'bible_xml/NKJV.xml',
BibleTranslation.nlt: 'bible_xml/NLT.xml',
BibleTranslation.nasb: 'bible_xml/NASB.xml',
BibleTranslation.kjv: 'bible_xml/KJV.xml',
BibleTranslation.msg: 'bible_xml/MSG.xml',
};
List<Scripture> _menstrualScriptures = [];
List<Scripture> _follicularScriptures = [];
List<Scripture> _ovulationScriptures = [];
List<Scripture> _lutealScriptures = [];
List<Scripture> _husbandScriptures = [];
List<Scripture> _womanhoodScriptures = [];
Map<String, List<Scripture>> _contextualScriptures = {};
// Hardcoded scriptures to ensure rich Husband experience immediately
final List<Scripture> _hardcodedHusbandScriptures = [
Scripture(
reference: "Mark 10:45",
verses: {
BibleTranslation.esv: "For even the Son of Man came not to be served but to serve, and to give his life as a ransom for many.",
BibleTranslation.niv: "For even the Son of Man did not come to be served, but to serve, and to give his life as a ransom for many.",
},
reflection: "True leadership is servanthood. How can you serve your wife today?",
applicablePhases: ['husband'],
applicableContexts: ['leadership', 'servant'],
),
Scripture(
reference: "Philippians 2:3-4",
verses: {
BibleTranslation.esv: "Do nothing from selfish ambition or conceit, but in humility count others more significant than yourselves. Let each of you look not only to his own interests, but also to the interests of others.",
},
reflection: "Humility is the foundation of a happy marriage.",
applicablePhases: ['husband'],
applicableContexts: ['servant', 'humility'],
),
Scripture(
reference: "Proverbs 29:18",
verses: {
BibleTranslation.esv: "Where there is no prophetic vision the people cast off restraint, but blessed is he who keeps the law.",
BibleTranslation.kjv: "Where there is no vision, the people perish: but he that keepeth the law, happy is he.",
},
reflection: "Lead your family with a clear, Godly vision.",
applicablePhases: ['husband'],
applicableContexts: ['vision', 'leadership'],
),
Scripture(
reference: "James 1:5",
verses: {
BibleTranslation.esv: "If any of you lacks wisdom, let him ask God, who gives generously to all without reproach, and it will be given him.",
},
reflection: "Seek God's wisdom in every decision you make for your family.",
applicablePhases: ['husband'],
applicableContexts: ['wisdom', 'vision'],
),
Scripture(
reference: "1 Timothy 3:4-5",
verses: {
BibleTranslation.esv: "He must manage his own household well, with all dignity keeping his children submissive, for if someone does not know how to manage his own household, how will he care for God's church?",
},
reflection: "Your first ministry is your home. Manage it with love and dignity.",
applicablePhases: ['husband'],
applicableContexts: ['leadership'],
),
Scripture(
reference: "Colossians 3:19",
verses: {
BibleTranslation.esv: "Husbands, love your wives, and do not be harsh with them.",
BibleTranslation.niv: "Husbands, love your wives and do not be harsh with them.",
},
reflection: "Gentleness is a sign of strength, not weakness.",
applicablePhases: ['husband'],
applicableContexts: ['kindness', 'love'],
),
Scripture(
reference: "1 Corinthians 16:14",
verses: {
BibleTranslation.esv: "Let all that you do be done in love.",
},
reflection: "Let love be the motivation behind every action and word.",
applicablePhases: ['husband'],
applicableContexts: ['love'],
),
];
Future<void> loadScriptures() async {
_scriptureBox = await Hive.openBox<Scripture>('scriptures');
if (_scriptureBox.isEmpty) {
print('Hive box is empty. Importing scriptures from optimized JSON data...');
// Load the pre-processed JSON file which already contains all verse text
final String response = await rootBundle.loadString('assets/scriptures_optimized.json');
final Map<String, dynamic> data = json.decode(response);
List<Scripture> importedScriptures = [];
// Helper function to process ANY list of scriptures
void processList(List<dynamic> list, String listName) {
for (var jsonEntry in list) {
final reference = jsonEntry['reference'];
final reflection = jsonEntry['reflection']; // Optional
final applicablePhases = (jsonEntry['applicablePhases'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ?? [];
final applicableContexts = (jsonEntry['applicableContexts'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ?? [];
// Map string keys (esv, niv) to BibleTranslation enum
Map<BibleTranslation, String> versesMap = {};
if (jsonEntry['verses'] != null) {
(jsonEntry['verses'] as Map<String, dynamic>).forEach((key, value) {
// Find enum by name (case-insensitive usually, but here keys are lowercase 'esv')
try {
final translation = BibleTranslation.values.firstWhere(
(e) => e.name.toLowerCase() == key.toLowerCase()
);
versesMap[translation] = value.toString();
} catch (e) {
print('Warning: Unknown translation key "$key" in optimized JSON');
}
});
}
if (versesMap.isNotEmpty) {
importedScriptures.add(Scripture(
verses: versesMap,
reference: reference,
reflection: reflection,
applicablePhases: applicablePhases,
applicableContexts: applicableContexts,
));
}
}
}
// Process all sections
if (data['menstrual'] != null) processList(data['menstrual'], 'menstrual');
if (data['follicular'] != null) processList(data['follicular'], 'follicular');
if (data['ovulation'] != null) processList(data['ovulation'], 'ovulation');
if (data['luteal'] != null) processList(data['luteal'], 'luteal');
if (data['husband'] != null) processList(data['husband'], 'husband');
if (data['womanhood'] != null) processList(data['womanhood'], 'womanhood');
if (data['contextual'] != null) {
final contextualMap = data['contextual'] as Map<String, dynamic>;
contextualMap.forEach((key, value) {
processList(value as List, 'contextual_$key');
});
}
// Store all imported scriptures into Hive
for (var scripture in importedScriptures) {
await _scriptureBox.put(scripture.reference, scripture); // Using reference as key
}
} else {
print('Hive box is not empty. Loading scriptures from Hive...');
}
// Populate internal lists from Hive box values
_menstrualScriptures = _scriptureBox.values
.where((s) => s.applicablePhases.contains('menstrual'))
.toList();
_follicularScriptures = _scriptureBox.values
.where((s) => s.applicablePhases.contains('follicular'))
.toList();
_ovulationScriptures = _scriptureBox.values
.where((s) => s.applicablePhases.contains('ovulation'))
.toList();
_lutealScriptures = _scriptureBox.values
.where((s) => s.applicablePhases.contains('luteal'))
.toList();
_husbandScriptures = [
..._scriptureBox.values.where((s) => s.applicablePhases.contains('husband')),
..._hardcodedHusbandScriptures,
];
// Remove duplicates based on reference if any
final uniqueHusbandIds = <String>{};
_husbandScriptures = _husbandScriptures.where((s) {
if (uniqueHusbandIds.contains(s.reference)) return false;
uniqueHusbandIds.add(s.reference);
return true;
}).toList();
_womanhoodScriptures = _scriptureBox.values
.where((s) => s.applicableContexts.contains('womanhood'))
.toList();
_contextualScriptures = {
'anxiety': _scriptureBox.values.where((s) => s.applicableContexts.contains('anxiety')).toList(),
'pain': _scriptureBox.values.where((s) => s.applicableContexts.contains('pain')).toList(),
'fatigue': _scriptureBox.values.where((s) => s.applicableContexts.contains('fatigue')).toList(),
'joy': _scriptureBox.values.where((s) => s.applicableContexts.contains('joy')).toList(),
};
}
/// Get the number of scriptures for a given phase
int getScriptureCountForPhase(String phase) {
switch (phase.toLowerCase()) {
case 'menstrual':
return _menstrualScriptures.length;
case 'follicular':
return _follicularScriptures.length;
case 'ovulation':
return _ovulationScriptures.length;
case 'luteal':
return _lutealScriptures.length;
case 'husband':
return _husbandScriptures.length;
case 'womanhood':
return _womanhoodScriptures.length;
case 'anxiety':
return _contextualScriptures['anxiety']?.length ?? 0;
case 'pain':
return _contextualScriptures['pain']?.length ?? 0;
case 'fatigue':
return _contextualScriptures['fatigue']?.length ?? 0;
case 'joy':
return _contextualScriptures['joy']?.length ?? 0;
default:
return 0;
}
}
/// Get recommended scripture based on entry
Scripture? getRecommendedScripture(CycleEntry entry) {
if (entry.mood == MoodLevel.verySad ||
entry.mood == MoodLevel.sad ||
(entry.stressLevel != null && entry.stressLevel! > 3)) {
final scriptures = _contextualScriptures['anxiety'];
if (scriptures != null && scriptures.isNotEmpty) {
return scriptures[DateTime.now().day % scriptures.length];
}
}
if ((entry.crampIntensity != null && entry.crampIntensity! >= 3) ||
entry.hasHeadache ||
entry.hasLowerBackPain) {
final scriptures = _contextualScriptures['pain'];
if (scriptures != null && scriptures.isNotEmpty) {
return scriptures[DateTime.now().day % scriptures.length];
}
}
if (entry.hasFatigue ||
entry.hasInsomnia ||
(entry.energyLevel != null && entry.energyLevel! <= 2)) {
final scriptures = _contextualScriptures['fatigue'];
if (scriptures != null && scriptures.isNotEmpty) {
return scriptures[DateTime.now().day % scriptures.length];
}
}
if (entry.mood == MoodLevel.veryHappy) {
final scriptures = _contextualScriptures['joy'];
if (scriptures != null && scriptures.isNotEmpty) {
return scriptures[DateTime.now().day % scriptures.length];
}
}
return null;
}
/// Get scripture for current phase by index
Scripture? getScriptureForPhaseByIndex(String phase, int index) {
List<Scripture> scriptures;
switch (phase.toLowerCase()) {
case 'menstrual':
scriptures = _menstrualScriptures;
break;
case 'follicular':
scriptures = _follicularScriptures;
break;
case 'ovulation':
scriptures = _ovulationScriptures;
break;
case 'luteal':
scriptures = _lutealScriptures;
break;
case 'husband':
scriptures = _husbandScriptures;
break;
case 'womanhood':
scriptures = _womanhoodScriptures;
break;
case 'anxiety':
scriptures = _contextualScriptures['anxiety'] ?? [];
break;
case 'pain':
scriptures = _contextualScriptures['pain'] ?? [];
break;
case 'fatigue':
scriptures = _contextualScriptures['fatigue'] ?? [];
break;
case 'joy':
scriptures = _contextualScriptures['joy'] ?? [];
break;
default:
return null;
}
if (scriptures.isEmpty || index < 0 || index >= scriptures.length) {
return null;
}
return scriptures[index];
}
// ... imports
// ... inside ScriptureDatabase class
/// Get a random scripture for a given phase
Scripture? getRandomScriptureForPhase(String phase) {
List<Scripture> scriptures;
switch (phase.toLowerCase()) {
case 'menstrual':
scriptures = _menstrualScriptures;
break;
case 'follicular':
scriptures = _follicularScriptures;
break;
case 'ovulation':
scriptures = _ovulationScriptures;
break;
case 'luteal':
scriptures = _lutealScriptures;
break;
case 'husband':
scriptures = _husbandScriptures;
break;
case 'womanhood':
scriptures = _womanhoodScriptures;
break;
case 'anxiety':
scriptures = _contextualScriptures['anxiety'] ?? [];
break;
case 'pain':
scriptures = _contextualScriptures['pain'] ?? [];
break;
case 'fatigue':
scriptures = _contextualScriptures['fatigue'] ?? [];
break;
case 'joy':
scriptures = _contextualScriptures['joy'] ?? [];
break;
default:
return null;
}
if (scriptures.isEmpty) {
return null;
}
return scriptures[Random().nextInt(scriptures.length)];
}
/// Get scripture for current phase (Randomized)
Scripture getScriptureForPhase(String phase) {
List<Scripture> scriptures;
switch (phase.toLowerCase()) {
// ... (same switch cases)
case 'menstrual':
scriptures = _menstrualScriptures;
break;
case 'follicular':
scriptures = _follicularScriptures;
break;
case 'ovulation':
scriptures = _ovulationScriptures;
break;
case 'luteal':
scriptures = _lutealScriptures;
break;
case 'husband':
scriptures = _husbandScriptures;
break;
case 'womanhood':
scriptures = _womanhoodScriptures;
break;
case 'anxiety':
scriptures = _contextualScriptures['anxiety'] ?? [];
break;
case 'pain':
scriptures = _contextualScriptures['pain'] ?? [];
break;
case 'fatigue':
scriptures = _contextualScriptures['fatigue'] ?? [];
break;
case 'joy':
scriptures = _contextualScriptures['joy'] ?? [];
break;
default:
// Fallback
scriptures = [
..._menstrualScriptures,
..._follicularScriptures,
..._ovulationScriptures,
..._lutealScriptures,
..._husbandScriptures,
..._womanhoodScriptures,
...(_contextualScriptures['anxiety'] ?? []),
...(_contextualScriptures['pain'] ?? []),
...(_contextualScriptures['fatigue'] ?? []),
...(_contextualScriptures['joy'] ?? []),
];
if (scriptures.isEmpty) return Scripture(verses: {BibleTranslation.esv: "No scripture found."}, reference: "Unknown", applicablePhases: [], applicableContexts: []);
}
if (scriptures.isEmpty) return Scripture(verses: {BibleTranslation.esv: "No scripture found."}, reference: "Unknown", applicablePhases: [], applicableContexts: []);
return scriptures[Random().nextInt(scriptures.length)];
}
/// Get scripture for husband (Randomized)
Scripture getHusbandScripture() {
final scriptures = _husbandScriptures;
if (scriptures.isEmpty) {
return Scripture(verses: {BibleTranslation.esv: "No husband scripture found."}, reference: "Unknown", applicablePhases: [], applicableContexts: []);
}
return scriptures[Random().nextInt(scriptures.length)];
}
/// Get all scriptures
List<Scripture> getAllScriptures() {
return _scriptureBox.values.toList();
}
}

View File

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

View File

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

View File

@@ -0,0 +1,285 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_profile.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class UserProfileAdapter extends TypeAdapter<UserProfile> {
@override
final int typeId = 2;
@override
UserProfile read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return UserProfile(
id: fields[0] as String,
name: fields[1] as String,
relationshipStatus: fields[2] as RelationshipStatus,
fertilityGoal: fields[3] as FertilityGoal?,
averageCycleLength: fields[4] as int,
averagePeriodLength: fields[5] as int,
lastPeriodStartDate: fields[6] as DateTime?,
notificationsEnabled: fields[7] as bool,
devotionalTime: fields[8] as String?,
hasCompletedOnboarding: fields[9] as bool,
createdAt: fields[10] as DateTime,
updatedAt: fields[11] as DateTime,
partnerName: fields[12] as String?,
role: fields[14] == null ? UserRole.wife : fields[14] as UserRole,
isIrregularCycle: fields[15] == null ? false : fields[15] as bool,
bibleTranslation: fields[16] == null
? BibleTranslation.esv
: fields[16] as BibleTranslation,
favoriteFoods: (fields[17] as List?)?.cast<String>(),
isDataShared: fields[18] == null ? false : fields[18] as bool,
);
}
@override
void write(BinaryWriter writer, UserProfile obj) {
writer
..writeByte(18)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.name)
..writeByte(2)
..write(obj.relationshipStatus)
..writeByte(3)
..write(obj.fertilityGoal)
..writeByte(4)
..write(obj.averageCycleLength)
..writeByte(5)
..write(obj.averagePeriodLength)
..writeByte(6)
..write(obj.lastPeriodStartDate)
..writeByte(7)
..write(obj.notificationsEnabled)
..writeByte(8)
..write(obj.devotionalTime)
..writeByte(9)
..write(obj.hasCompletedOnboarding)
..writeByte(10)
..write(obj.createdAt)
..writeByte(11)
..write(obj.updatedAt)
..writeByte(12)
..write(obj.partnerName)
..writeByte(14)
..write(obj.role)
..writeByte(15)
..write(obj.isIrregularCycle)
..writeByte(16)
..write(obj.bibleTranslation)
..writeByte(17)
..write(obj.favoriteFoods)
..writeByte(18)
..write(obj.isDataShared);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserProfileAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class RelationshipStatusAdapter extends TypeAdapter<RelationshipStatus> {
@override
final int typeId = 0;
@override
RelationshipStatus read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return RelationshipStatus.single;
case 1:
return RelationshipStatus.engaged;
case 2:
return RelationshipStatus.married;
default:
return RelationshipStatus.single;
}
}
@override
void write(BinaryWriter writer, RelationshipStatus obj) {
switch (obj) {
case RelationshipStatus.single:
writer.writeByte(0);
break;
case RelationshipStatus.engaged:
writer.writeByte(1);
break;
case RelationshipStatus.married:
writer.writeByte(2);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RelationshipStatusAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class FertilityGoalAdapter extends TypeAdapter<FertilityGoal> {
@override
final int typeId = 1;
@override
FertilityGoal read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return FertilityGoal.tryingToConceive;
case 1:
return FertilityGoal.tryingToAvoid;
case 2:
return FertilityGoal.justTracking;
default:
return FertilityGoal.tryingToConceive;
}
}
@override
void write(BinaryWriter writer, FertilityGoal obj) {
switch (obj) {
case FertilityGoal.tryingToConceive:
writer.writeByte(0);
break;
case FertilityGoal.tryingToAvoid:
writer.writeByte(1);
break;
case FertilityGoal.justTracking:
writer.writeByte(2);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FertilityGoalAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class BibleTranslationAdapter extends TypeAdapter<BibleTranslation> {
@override
final int typeId = 9;
@override
BibleTranslation read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return BibleTranslation.esv;
case 1:
return BibleTranslation.niv;
case 2:
return BibleTranslation.nkjv;
case 3:
return BibleTranslation.nlt;
case 4:
return BibleTranslation.nasb;
case 5:
return BibleTranslation.kjv;
case 6:
return BibleTranslation.msg;
default:
return BibleTranslation.esv;
}
}
@override
void write(BinaryWriter writer, BibleTranslation obj) {
switch (obj) {
case BibleTranslation.esv:
writer.writeByte(0);
break;
case BibleTranslation.niv:
writer.writeByte(1);
break;
case BibleTranslation.nkjv:
writer.writeByte(2);
break;
case BibleTranslation.nlt:
writer.writeByte(3);
break;
case BibleTranslation.nasb:
writer.writeByte(4);
break;
case BibleTranslation.kjv:
writer.writeByte(5);
break;
case BibleTranslation.msg:
writer.writeByte(6);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BibleTranslationAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class UserRoleAdapter extends TypeAdapter<UserRole> {
@override
final int typeId = 8;
@override
UserRole read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return UserRole.wife;
case 1:
return UserRole.husband;
default:
return UserRole.wife;
}
}
@override
void write(BinaryWriter writer, UserRole obj) {
switch (obj) {
case UserRole.wife:
writer.writeByte(0);
break;
case UserRole.husband:
writer.writeByte(1);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserRoleAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// Provider to manage the bottom navigation index across the app
final navigationProvider = StateNotifierProvider<NavigationNotifier, int>((ref) {
return NavigationNotifier();
});
class NavigationNotifier extends StateNotifier<int> {
NavigationNotifier() : super(0);
void setIndex(int index) {
state = index;
}
}

View File

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

View File

@@ -0,0 +1,81 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../models/user_profile.dart';
import '../models/cycle_entry.dart';
import '../services/cycle_service.dart';
/// Provider for the user profile
final userProfileProvider = StateNotifierProvider<UserProfileNotifier, UserProfile?>((ref) {
return UserProfileNotifier();
});
/// Notifier for the user profile
class UserProfileNotifier extends StateNotifier<UserProfile?> {
UserProfileNotifier() : super(null) {
_loadProfile();
}
void _loadProfile() {
final box = Hive.box<UserProfile>('user_profile');
state = box.get('current_user');
}
Future<void> updateProfile(UserProfile profile) async {
final box = Hive.box<UserProfile>('user_profile');
await box.put('current_user', profile);
state = profile;
}
Future<void> clearProfile() async {
final box = Hive.box<UserProfile>('user_profile');
await box.clear();
state = null;
}
}
/// Provider for cycle entries
final cycleEntriesProvider = StateNotifierProvider<CycleEntriesNotifier, List<CycleEntry>>((ref) {
return CycleEntriesNotifier();
});
/// Notifier for cycle entries
class CycleEntriesNotifier extends StateNotifier<List<CycleEntry>> {
CycleEntriesNotifier() : super([]) {
_loadEntries();
}
void _loadEntries() {
final box = Hive.box<CycleEntry>('cycle_entries');
state = box.values.toList()..sort((a, b) => b.date.compareTo(a.date));
}
Future<void> addEntry(CycleEntry entry) async {
final box = Hive.box<CycleEntry>('cycle_entries');
await box.put(entry.id, entry);
_loadEntries();
}
Future<void> updateEntry(CycleEntry entry) async {
final box = Hive.box<CycleEntry>('cycle_entries');
await box.put(entry.id, entry);
_loadEntries();
}
Future<void> deleteEntry(String id) async {
final box = Hive.box<CycleEntry>('cycle_entries');
await box.delete(id);
_loadEntries();
}
Future<void> clearEntries() async {
final box = Hive.box<CycleEntry>('cycle_entries');
await box.clear();
state = [];
}
}
/// Computed provider for current cycle info
final currentCycleInfoProvider = Provider((ref) {
final user = ref.watch(userProfileProvider);
return CycleService.calculateCycleInfo(user);
});

View File

@@ -0,0 +1,645 @@
import 'package:christian_period_tracker/models/scripture.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
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';
class CalendarScreen extends ConsumerStatefulWidget {
final bool readOnly;
const CalendarScreen({
super.key,
this.readOnly = false,
});
@override
ConsumerState<CalendarScreen> createState() => _CalendarScreenState();
}
class _CalendarScreenState extends ConsumerState<CalendarScreen> {
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
CalendarFormat _calendarFormat = CalendarFormat.month;
@override
Widget build(BuildContext context) {
final entries = ref.watch(cycleEntriesProvider);
final user = ref.watch(userProfileProvider);
final cycleLength = user?.averageCycleLength ?? 28;
final lastPeriodStart = user?.lastPeriodStartDate;
return SafeArea(
child: Column(
children: [
// Header
Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Expanded(
child: Text(
'Calendar',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
),
_buildLegendButton(),
],
),
),
// Calendar
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.05),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: TableCalendar(
firstDay: DateTime.now().subtract(const Duration(days: 365)),
lastDay: DateTime.now().add(const Duration(days: 365)),
focusedDay: _focusedDay,
calendarFormat: _calendarFormat,
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
},
onFormatChanged: (format) {
setState(() => _calendarFormat = format);
},
onPageChanged: (focusedDay) {
_focusedDay = focusedDay;
},
calendarStyle: CalendarStyle(
outsideDaysVisible: false,
defaultTextStyle: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.charcoal,
),
weekendTextStyle: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.charcoal,
),
todayDecoration: BoxDecoration(
color: AppColors.sageGreen.withOpacity(0.3),
shape: BoxShape.circle,
),
todayTextStyle: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.sageGreen,
),
selectedDecoration: const BoxDecoration(
color: AppColors.sageGreen,
shape: BoxShape.circle,
),
selectedTextStyle: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
headerStyle: HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
titleTextStyle: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
leftChevronIcon: Icon(
Icons.chevron_left,
color: AppColors.warmGray,
),
rightChevronIcon: Icon(
Icons.chevron_right,
color: AppColors.warmGray,
),
),
daysOfWeekStyle: DaysOfWeekStyle(
weekdayStyle: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
weekendStyle: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
calendarBuilders: CalendarBuilders(
markerBuilder: (context, date, events) {
final entry = _getEntryForDate(date, entries);
if (entry == null) {
final phase =
_getPhaseForDate(date, lastPeriodStart, cycleLength);
if (phase != null) {
return Positioned(
bottom: 1,
child: Container(
width: 4,
height: 4,
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.3),
shape: BoxShape.circle,
),
),
);
}
return null;
}
// If we have an entry, show icons/markers
return Positioned(
bottom: 1,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (entry.isPeriodDay)
Container(
width: 6,
height: 6,
margin: const EdgeInsets.symmetric(horizontal: 1),
decoration: const BoxDecoration(
color: AppColors.menstrualPhase,
shape: BoxShape.circle,
),
),
if (entry.mood != null ||
entry.energyLevel != 3 ||
entry.hasSymptoms)
Container(
width: 6,
height: 6,
margin: const EdgeInsets.symmetric(horizontal: 1),
decoration: const BoxDecoration(
color: AppColors.softGold,
shape: BoxShape.circle,
),
),
],
),
);
},
),
),
),
const SizedBox(height: 20),
// Selected Day Info
if (_selectedDay != null)
Expanded(
child: _buildDayInfo(
_selectedDay!, lastPeriodStart, cycleLength, entries),
),
],
),
);
}
Widget _buildLegendButton() {
return GestureDetector(
onTap: () => _showLegend(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: AppColors.blushPink.withOpacity(0.5),
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
Icon(Icons.info_outline, size: 16, color: AppColors.rose),
const SizedBox(width: 4),
Text(
'Legend',
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.rose,
),
),
],
),
),
);
}
void _showLegend() {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Legend',
style: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 20),
_buildLegendItem(AppColors.menstrualPhase, 'Period'),
_buildLegendItem(AppColors.follicularPhase, 'Follicular Phase'),
_buildLegendItem(AppColors.ovulationPhase, 'Ovulation Window'),
_buildLegendItem(AppColors.lutealPhase, 'Luteal Phase'),
const SizedBox(height: 20),
],
),
),
);
}
Widget _buildLegendItem(Color color, String label) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 12),
Text(
label,
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.charcoal,
),
),
],
),
);
}
Widget _buildDayInfo(DateTime date, DateTime? lastPeriodStart, int cycleLength,
List<CycleEntry> entries) {
final phase = _getPhaseForDate(date, lastPeriodStart, cycleLength);
final entry = _getEntryForDate(date, entries);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Theme.of(context).cardTheme.color,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_getMonthName(date.month)} ${date.day}, ${date.year}',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
if (entry != null)
const Icon(Icons.check_circle,
color: AppColors.sageGreen, size: 20),
],
),
const SizedBox(height: 16),
if (phase != null)
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(phase.emoji),
const SizedBox(width: 6),
Text(
phase.label,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: _getPhaseColor(phase),
),
),
],
),
),
),
if (entry == null)
Text(
phase?.description ?? 'No data for this date',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: AppColors.warmGray),
)
else ...[
// Period Detail
if (entry.isPeriodDay)
_buildDetailRow(Icons.water_drop, 'Period Day',
AppColors.menstrualPhase,
value: entry.flowIntensity?.label),
// Mood Detail
if (entry.mood != null)
_buildDetailRow(
Icons.emoji_emotions_outlined, 'Mood', AppColors.softGold,
value: '${entry.mood!.emoji} ${entry.mood!.label}'),
// Energy Detail
_buildDetailRow(
Icons.flash_on, 'Energy Level', AppColors.follicularPhase,
value: _getEnergyLabel(entry.energyLevel)),
// Symptoms
if (entry.hasSymptoms)
_buildDetailRow(
Icons.healing_outlined, 'Symptoms', AppColors.lavender,
value: _getSymptomsString(entry)),
// Contextual Recommendation
_buildRecommendation(entry),
// Notes
if (entry.notes?.isNotEmpty == true)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Notes',
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.warmGray)),
const SizedBox(height: 4),
Text(entry.notes!,
style: GoogleFonts.outfit(fontSize: 14)),
],
),
),
],
const SizedBox(height: 24),
// Action Buttons
if (!widget.readOnly)
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(
title: Text(
'Log for ${_getMonthName(date.month)} ${date.day}'),
),
body: LogScreen(initialDate: date),
),
),
);
},
icon: Icon(entry != null
? Icons.edit_note
: Icons.add_circle_outline),
label: Text(entry != null ? 'Edit Log' : 'Add Log'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.sageGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
),
),
],
),
],
),
);
}
Widget _buildRecommendation(CycleEntry entry) {
final scripture = ScriptureDatabase().getRecommendedScripture(entry);
if (scripture == null) return const SizedBox.shrink();
final user = ref.read(userProfileProvider);
final translation = user?.bibleTranslation ?? BibleTranslation.esv;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
margin: const EdgeInsets.only(top: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.softGold.withOpacity(isDark ? 0.15 : 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.softGold.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.auto_awesome,
color: AppColors.softGold, size: 18),
const SizedBox(width: 8),
Text(
'Daily Encouragement',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.softGold,
),
),
],
),
const SizedBox(height: 12),
Text(
'"${scripture.getVerse(translation)}"',
style: GoogleFonts.lora(
fontSize: 14,
fontStyle: FontStyle.italic,
color: isDark ? Colors.white : AppColors.charcoal,
height: 1.5,
),
),
const SizedBox(height: 8),
Text(
'${scripture.reference}',
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
],
),
);
}
Widget _buildDetailRow(IconData icon, String label, Color color,
{String? value}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 18),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: GoogleFonts.outfit(
fontSize: 12,
color: AppColors.warmGray,
),
),
if (value != null)
Text(
value,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
);
}
String _getSymptomsString(CycleEntry entry) {
List<String> s = [];
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');
return s.join(', ');
}
String _getEnergyLabel(int? energyLevel) {
if (energyLevel == null) return 'Not logged';
if (energyLevel <= 1) return 'Very Low';
if (energyLevel == 2) return 'Low';
if (energyLevel == 3) return 'Neutral';
if (energyLevel == 4) return 'High';
return 'Very High';
}
CyclePhase? _getPhaseForDate(
DateTime date, DateTime? lastPeriodStart, int cycleLength) {
if (lastPeriodStart == null) return null;
final daysSinceLastPeriod = date.difference(lastPeriodStart).inDays;
if (daysSinceLastPeriod < 0) return null;
final dayOfCycle = (daysSinceLastPeriod % cycleLength) + 1;
if (dayOfCycle <= 5) return CyclePhase.menstrual;
if (dayOfCycle <= 13) return CyclePhase.follicular;
if (dayOfCycle <= 16) return CyclePhase.ovulation;
return CyclePhase.luteal;
}
Color _getPhaseColor(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return AppColors.menstrualPhase;
case CyclePhase.follicular:
return AppColors.follicularPhase;
case CyclePhase.ovulation:
return AppColors.ovulationPhase;
case CyclePhase.luteal:
return AppColors.lutealPhase;
}
}
String _getMonthName(int month) {
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
return months[month - 1];
}
bool _isLoggedPeriodDay(DateTime date, List<CycleEntry> entries) {
final entry = _getEntryForDate(date, entries);
return entry?.isPeriodDay ?? false;
}
CycleEntry? _getEntryForDate(DateTime date, List<CycleEntry> entries) {
try {
return entries.firstWhere(
(entry) => isSameDay(entry.date, date),
);
} catch (_) {
return null;
}
}
}

View File

@@ -0,0 +1,434 @@
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';
import '../../models/cycle_entry.dart';
import '../../theme/app_theme.dart';
import '../../widgets/scripture_card.dart';
import '../../models/user_profile.dart';
import '../../providers/scripture_provider.dart'; // Import the new provider
class DevotionalScreen extends ConsumerStatefulWidget {
const DevotionalScreen({super.key});
@override
ConsumerState<DevotionalScreen> createState() => _DevotionalScreenState();
}
class _DevotionalScreenState extends ConsumerState<DevotionalScreen> {
@override
void initState() {
super.initState();
_initializeScripture();
}
Future<void> _initializeScripture() async {
final phase = ref.read(currentCycleInfoProvider).phase;
await ref.read(scriptureProvider.notifier).initializeScripture(phase);
}
Future<void> _showTranslationPicker(
BuildContext context, WidgetRef ref, UserProfile? user) async {
if (user == null) return;
final selected = await showModalBottomSheet<BibleTranslation>(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => Container(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Text(
'Select Bible Translation',
style: Theme.of(context).textTheme.titleLarge,
),
),
...BibleTranslation.values.map((t) => ListTile(
title: Text(t.label),
trailing: user.bibleTranslation == t
? Icon(Icons.check, color: AppColors.sageGreen)
: null,
onTap: () => Navigator.pop(context, t),
)),
],
),
),
);
if (selected != null) {
await ref
.read(userProfileProvider.notifier)
.updateProfile(user.copyWith(bibleTranslation: selected));
}
}
@override
Widget build(BuildContext context) {
// Listen for changes in the cycle info to re-initialize scripture if needed
ref.listen<CycleInfo>(currentCycleInfoProvider, (previousCycleInfo, newCycleInfo) {
if (previousCycleInfo?.phase != newCycleInfo.phase) {
_initializeScripture();
}
});
final user = ref.watch(userProfileProvider);
final cycleInfo = ref.watch(currentCycleInfoProvider);
final phase = cycleInfo.phase;
// Watch the scripture provider for the current scripture
final scriptureState = ref.watch(scriptureProvider);
final scripture = scriptureState.currentScripture;
final maxIndex = scriptureState.maxIndex;
if (scripture == null) {
return const Center(child: CircularProgressIndicator()); // Or some error message
}
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Expanded(
child: Text(
'Today\'s Devotional',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
Text(phase.emoji),
const SizedBox(width: 4),
Text(
phase.label,
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: _getPhaseColor(phase),
),
),
],
),
),
],
),
const SizedBox(height: 8),
Text(
phase.description,
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
const SizedBox(height: 32),
// Main Scripture Card with Navigation
Stack(
alignment: Alignment.center,
children: [
ScriptureCard(
verse: scripture
.getVerse(user?.bibleTranslation ?? BibleTranslation.esv),
reference: scripture.reference,
translation:
(user?.bibleTranslation ?? BibleTranslation.esv).label,
phase: phase,
onTranslationTap: () =>
_showTranslationPicker(context, ref, user),
),
if (maxIndex != null && maxIndex > 1) ...[
Positioned(
left: 0,
child: IconButton(
icon: Icon(Icons.arrow_back_ios),
onPressed: () =>
ref.read(scriptureProvider.notifier).getPreviousScripture(),
color: AppColors.charcoal,
),
),
Positioned(
right: 0,
child: IconButton(
icon: Icon(Icons.arrow_forward_ios),
onPressed: () =>
ref.read(scriptureProvider.notifier).getNextScripture(),
color: AppColors.charcoal,
),
),
],
],
),
const SizedBox(height: 16),
if (maxIndex != null && maxIndex > 1)
Center(
child: TextButton.icon(
onPressed: () => ref.read(scriptureProvider.notifier).getRandomScripture(),
icon: const Icon(Icons.shuffle),
label: const Text('Random Verse'),
style: TextButton.styleFrom(
foregroundColor: AppColors.sageGreen,
),
),
),
const SizedBox(height: 24),
// Reflection
if (scripture.reflection != null) ...[
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.lightbulb_outline,
color: AppColors.softGold,
size: 20,
),
const SizedBox(width: 8),
Text(
'Reflection',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
],
),
const SizedBox(height: 12),
Text(
scripture.reflection!,
style: GoogleFonts.outfit(
fontSize: 15,
color: AppColors.charcoal,
height: 1.6,
),
),
],
),
),
const SizedBox(height: 16),
],
// Phase-specific encouragement
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.favorite_outline,
color: AppColors.rose,
size: 20,
),
const SizedBox(width: 8),
Text(
'For Your ${phase.label} Phase',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
],
),
const SizedBox(height: 12),
Text(
_getPhaseEncouragement(phase, user?.isMarried ?? false),
style: GoogleFonts.outfit(
fontSize: 15,
color: AppColors.charcoal,
height: 1.6,
),
),
],
),
),
const SizedBox(height: 16),
// Prayer Prompt
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.lavender.withOpacity(0.2),
AppColors.blushPink.withOpacity(0.2),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('🙏', style: TextStyle(fontSize: 20)),
const SizedBox(width: 8),
Text(
'Prayer Prompt',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
],
),
const SizedBox(height: 12),
Text(
_getPrayerPrompt(phase),
style: GoogleFonts.lora(
fontSize: 14,
fontStyle: FontStyle.italic,
color: AppColors.charcoal,
height: 1.6,
),
),
],
),
),
const SizedBox(height: 24),
// Action buttons
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: const Icon(Icons.share_outlined),
label: const Text('Share'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () {},
icon: const Icon(Icons.edit_note),
label: const Text('Journal'),
),
),
],
),
const SizedBox(height: 40),
],
),
),
);
}
// Placeholder _getCurrentPhase removed as it's now in CycleService
Color _getPhaseColor(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return AppColors.menstrualPhase;
case CyclePhase.follicular:
return AppColors.follicularPhase;
case CyclePhase.ovulation:
return AppColors.ovulationPhase;
case CyclePhase.luteal:
return AppColors.lutealPhase;
}
}
String _getPhaseEncouragement(CyclePhase phase, bool isMarried) {
switch (phase) {
case CyclePhase.menstrual:
return 'Your body is renewing itself. This is a sacred time for rest and reflection. '
'Don\'t push yourself too hard—God designed this phase for slowing down. '
'Use this time to draw near to Him in quietness.';
case CyclePhase.follicular:
return 'Energy is returning! Your body is preparing for the days ahead. '
'This is a wonderful time to tackle projects, connect with friends, and serve others. '
'Let your renewed strength be used for His purposes.';
case CyclePhase.ovulation:
if (isMarried) {
return 'You are in your most fertile window. Whether you\'re hoping to conceive or practicing NFP, '
'remember that God is sovereign over the womb. Trust His timing and purposes for your family.';
}
return 'You may feel more confident and social during this phase. '
'It\'s a great time for important conversations, presentations, or stepping out in faith. '
'Let your light shine before others.';
case CyclePhase.luteal:
return 'The luteal phase can bring challenging emotions and PMS symptoms. '
'Be patient with yourself. This is not weakness—it\'s your body doing what God designed. '
'Lean into His peace that surpasses understanding.';
}
}
String _getPrayerPrompt(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return '"Lord, thank You for designing my body with such wisdom. '
'Help me to rest in You during this time and to trust that You are renewing me. '
'May I find my strength in Your presence. Amen."';
case CyclePhase.follicular:
return '"Father, thank You for this season of renewed energy. '
'Guide me to use this strength for Your glory and the good of others. '
'Help me to serve with joy and purpose. Amen."';
case CyclePhase.ovulation:
return '"Creator God, I am fearfully and wonderfully made. '
'Thank You for the gift of womanhood. '
'Help me to honor You in all I do today. Amen."';
case CyclePhase.luteal:
return '"Lord, I bring my anxious thoughts to You. '
'When my emotions feel overwhelming, remind me of Your peace. '
'Help me to be gentle with myself as You are gentle with me. Amen."';
}
}
}

View File

@@ -0,0 +1,625 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../theme/app_theme.dart';
import '../../models/user_profile.dart';
import '../../models/cycle_entry.dart';
import '../../models/scripture.dart';
import '../calendar/calendar_screen.dart';
import '../log/log_screen.dart';
import '../devotional/devotional_screen.dart';
import '../../widgets/tip_card.dart';
import '../../widgets/cycle_ring.dart';
import '../../widgets/scripture_card.dart';
import '../../widgets/quick_log_buttons.dart';
import '../../providers/user_provider.dart';
import '../../providers/navigation_provider.dart';
import '../../services/cycle_service.dart';
import '../../services/bible_utils.dart';
import '../../providers/scripture_provider.dart'; // Import the new provider
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedIndex = ref.watch(navigationProvider);
return Scaffold(
body: IndexedStack(
index: selectedIndex,
children: [
const _DashboardTab(),
const CalendarScreen(),
const LogScreen(),
const DevotionalScreen(),
_SettingsTab(
onReset: () =>
ref.read(navigationProvider.notifier).setIndex(0)),
],
),
bottomNavigationBar: Container(
decoration: BoxDecoration(
color: Theme.of(context).bottomNavigationBarTheme.backgroundColor,
boxShadow: [
BoxShadow(
color: (Theme.of(context).brightness == Brightness.dark
? Colors.black
: AppColors.charcoal)
.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: BottomNavigationBar(
currentIndex: selectedIndex,
onTap: (index) =>
ref.read(navigationProvider.notifier).setIndex(index),
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.calendar_today_outlined),
activeIcon: Icon(Icons.calendar_today),
label: 'Calendar',
),
BottomNavigationBarItem(
icon: Icon(Icons.add_circle_outline),
activeIcon: Icon(Icons.add_circle),
label: 'Log',
),
BottomNavigationBarItem(
icon: Icon(Icons.menu_book_outlined),
activeIcon: Icon(Icons.menu_book),
label: 'Devotional',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
activeIcon: Icon(Icons.settings),
label: 'Settings',
),
],
),
),
);
}
}
class _DashboardTab extends ConsumerStatefulWidget {
const _DashboardTab({super.key});
@override
ConsumerState<_DashboardTab> createState() => _DashboardTabState();
}
class _DashboardTabState extends ConsumerState<_DashboardTab> {
@override
void initState() {
super.initState();
_initializeScripture();
}
// This method initializes the scripture and can react to phase changes.
// It's called from initState and also when currentCycleInfoProvider changes.
Future<void> _initializeScripture() async {
final phase = ref.read(currentCycleInfoProvider).phase;
await ref.read(scriptureProvider.notifier).initializeScripture(phase);
}
@override
Widget build(BuildContext context) {
// Listen for changes in the cycle info to re-initialize scripture if needed
ref.listen<CycleInfo>(currentCycleInfoProvider, (previousCycleInfo, newCycleInfo) {
if (previousCycleInfo?.phase != newCycleInfo.phase) {
_initializeScripture();
}
});
final name =
ref.watch(userProfileProvider.select((u) => u?.name)) ?? 'Friend';
final translation =
ref.watch(userProfileProvider.select((u) => u?.bibleTranslation)) ??
BibleTranslation.esv;
final role = ref.watch(userProfileProvider.select((u) => u?.role)) ??
UserRole.wife;
final isMarried =
ref.watch(userProfileProvider.select((u) => u?.isMarried)) ?? false;
final averageCycleLength =
ref.watch(userProfileProvider.select((u) => u?.averageCycleLength)) ??
28;
final cycleInfo = ref.watch(currentCycleInfoProvider);
final phase = cycleInfo.phase;
final dayOfCycle = cycleInfo.dayOfCycle;
// Watch the scripture provider for the current scripture
final scriptureState = ref.watch(scriptureProvider);
final scripture = scriptureState.currentScripture;
final maxIndex = scriptureState.maxIndex;
if (scripture == null) {
return const Center(child: CircularProgressIndicator()); // Or some error message
}
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildGreeting(context, name),
const SizedBox(height: 24),
Center(
child: CycleRing(
dayOfCycle: dayOfCycle,
totalDays: averageCycleLength,
phase: phase,
),
),
const SizedBox(height: 32),
// Main Scripture Card with Navigation
Stack(
alignment: Alignment.center,
children: [
ScriptureCard(
verse: scripture.getVerse(translation),
reference: scripture.reference,
translation: translation.label,
phase: phase,
onTranslationTap: () =>
BibleUtils.showTranslationPicker(context, ref),
),
if (maxIndex != null && maxIndex > 1) ...[
Positioned(
left: 0,
child: IconButton(
icon: Icon(Icons.arrow_back_ios),
onPressed: () =>
ref.read(scriptureProvider.notifier).getPreviousScripture(),
color: AppColors.charcoal,
),
),
Positioned(
right: 0,
child: IconButton(
icon: Icon(Icons.arrow_forward_ios),
onPressed: () =>
ref.read(scriptureProvider.notifier).getNextScripture(),
color: AppColors.charcoal,
),
),
],
],
),
const SizedBox(height: 16),
if (maxIndex != null && maxIndex > 1)
Center(
child: TextButton.icon(
onPressed: () => ref.read(scriptureProvider.notifier).getRandomScripture(),
icon: const Icon(Icons.shuffle),
label: const Text('Random Verse'),
style: TextButton.styleFrom(
foregroundColor: AppColors.sageGreen,
),
),
),
const SizedBox(height: 24),
Text(
'Quick Log',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
const QuickLogButtons(),
const SizedBox(height: 24),
if (role == UserRole.wife)
TipCard(phase: phase, isMarried: isMarried),
const SizedBox(height: 20),
],
),
),
);
}
Widget _buildGreeting(BuildContext context, String name) {
final theme = Theme.of(context);
final hour = DateTime.now().hour;
String greeting;
if (hour < 12) {
greeting = 'Good morning';
} else if (hour < 17) {
greeting = 'Good afternoon';
} else {
greeting = 'Good evening';
}
return Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$greeting,',
style: GoogleFonts.outfit(
fontSize: 16,
color: theme.colorScheme.onSurfaceVariant,
),
),
Text(
name,
style: theme.textTheme.displaySmall?.copyWith(
fontSize: 28,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
],
),
),
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.notifications_outlined,
color: theme.colorScheme.primary,
),
),
],
);
}
}
class _SettingsTab extends ConsumerWidget {
final VoidCallback? onReset;
const _SettingsTab({this.onReset});
Widget _buildSettingsTile(BuildContext context, IconData icon, String title,
{VoidCallback? onTap}) {
return ListTile(
leading: Icon(icon,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8)),
title: Text(
title,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontSize: 16,
),
),
trailing: const Icon(Icons.chevron_right, color: AppColors.lightGray),
onTap: onTap ??
() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Settings coming soon!')),
);
},
);
}
Future<void> _resetApp(BuildContext context, WidgetRef ref) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Reset App?'),
content: const Text(
'This will clear all data and return you to onboarding. Are you sure?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel')),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Reset', style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirmed == true) {
await ref.read(userProfileProvider.notifier).clearProfile();
await ref.read(cycleEntriesProvider.notifier).clearEntries();
if (context.mounted) {
onReset?.call();
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
}
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final name =
ref.watch(userProfileProvider.select((u) => u?.name)) ?? 'Guest';
final roleSymbol =
ref.watch(userProfileProvider.select((u) => u?.role)) ==
UserRole.husband
? 'HUSBAND'
: null;
final relationshipStatus = ref.watch(userProfileProvider
.select((u) => u?.relationshipStatus.name.toUpperCase())) ??
'SINGLE';
final translationLabel =
ref.watch(userProfileProvider.select((u) => u?.bibleTranslation.label)) ??
'ESV';
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Settings',
style: Theme.of(context).textTheme.displayMedium?.copyWith(
fontSize: 28,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Theme.of(context).cardTheme.color,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color:
Theme.of(context).colorScheme.outline.withOpacity(0.05)),
),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.blushPink,
AppColors.rose.withOpacity(0.7)
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Text(
name.isNotEmpty ? name[0].toUpperCase() : '?',
style: GoogleFonts.outfit(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style:
Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
Text(
roleSymbol ?? relationshipStatus,
style: GoogleFonts.outfit(
fontSize: 12,
letterSpacing: 1,
color: AppColors.warmGray,
),
),
],
),
),
const Icon(Icons.chevron_right, color: AppColors.warmGray),
],
),
),
const SizedBox(height: 24),
_buildSettingsGroup(context, 'Preferences', [
_buildSettingsTile(
context, Icons.notifications_outlined, 'Notifications'),
_buildSettingsTile(
context,
Icons.book_outlined,
'Bible Version ($translationLabel)',
onTap: () => BibleUtils.showTranslationPicker(context, ref),
),
_buildSettingsTile(context, Icons.palette_outlined, 'Appearance'),
_buildSettingsTile(
context,
Icons.favorite_border,
'My Favorites',
onTap: () => _showFavoritesDialog(context, ref),
),
_buildSettingsTile(context, Icons.lock_outline, 'Privacy'),
_buildSettingsTile(
context,
Icons.share_outlined,
'Share with Husband',
onTap: () => _showShareDialog(context, ref),
),
]),
const SizedBox(height: 16),
_buildSettingsGroup(context, 'Cycle', [
_buildSettingsTile(
context, Icons.calendar_today_outlined, 'Cycle Settings'),
_buildSettingsTile(
context, Icons.trending_up_outlined, 'Cycle History'),
_buildSettingsTile(
context, Icons.download_outlined, 'Export Data'),
]),
const SizedBox(height: 16),
_buildSettingsGroup(context, 'Account', [
_buildSettingsTile(context, Icons.logout, 'Reset App / Logout',
onTap: () => _resetApp(context, ref)),
]),
const SizedBox(height: 16),
],
),
),
);
}
Widget _buildSettingsGroup(
BuildContext context, String title, List<Widget> tiles) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
letterSpacing: 0.5,
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardTheme.color,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.05)),
),
child: Column(
children: tiles,
),
),
],
);
}
void _showFavoritesDialog(BuildContext context, WidgetRef ref) {
final userProfile = ref.read(userProfileProvider);
if (userProfile == null) return;
final controller = TextEditingController(
text: userProfile.favoriteFoods?.join(', ') ?? '',
);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('My Favorites', style: GoogleFonts.outfit(fontWeight: FontWeight.bold)),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'List your favorite comfort foods, snacks, or flowers so your husband knows what to get you!',
style: GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray),
),
const SizedBox(height: 16),
TextField(
controller: controller,
maxLines: 3,
decoration: const InputDecoration(
hintText: 'e.g., Dark Chocolate, Sushi, Sunflowers...',
border: OutlineInputBorder(),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
final favorites = controller.text
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
final updatedProfile = userProfile.copyWith(favoriteFoods: favorites);
ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
Navigator.pop(context);
},
child: const Text('Save'),
),
],
),
);
}
void _showShareDialog(BuildContext context, WidgetRef ref) {
// Generate a simple pairing code (in a real app, this would be stored/validated)
final userProfile = ref.read(userProfileProvider);
final pairingCode = userProfile?.id?.substring(0, 6).toUpperCase() ?? 'ABC123';
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
const Icon(Icons.share_outlined, color: AppColors.sageGreen),
const SizedBox(width: 8),
const Text('Share with Husband'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Share this code with your husband so he can connect to your cycle data:',
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
),
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration(
color: AppColors.sageGreen.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.sageGreen.withOpacity(0.3)),
),
child: SelectableText(
pairingCode,
style: GoogleFonts.outfit(
fontSize: 32,
fontWeight: FontWeight.bold,
letterSpacing: 4,
color: AppColors.sageGreen,
),
),
),
const SizedBox(height: 16),
Text(
'He can enter this in his app under Settings > Connect with Wife.',
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
textAlign: TextAlign.center,
),
],
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.sageGreen,
foregroundColor: Colors.white,
),
child: const Text('Done'),
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -0,0 +1,836 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../models/cycle_entry.dart';
import '../../providers/navigation_provider.dart';
import '../../providers/user_provider.dart';
import '../../theme/app_theme.dart';
import 'package:uuid/uuid.dart';
class LogScreen extends ConsumerStatefulWidget {
final DateTime? initialDate;
const LogScreen({super.key, this.initialDate});
@override
ConsumerState<LogScreen> createState() => _LogScreenState();
}
class _LogScreenState extends ConsumerState<LogScreen> {
late DateTime _selectedDate;
String? _existingEntryId;
bool _isPeriodDay = false;
FlowIntensity? _flowIntensity;
MoodLevel? _mood;
int? _energyLevel;
int _crampIntensity = 0;
bool _hasHeadache = false;
bool _hasBloating = false;
bool _hasBreastTenderness = false;
bool _hasFatigue = false;
bool _hasAcne = false;
bool _hasLowerBackPain = false;
bool _hasConstipation = false;
bool _hasDiarrhea = false;
bool _hasInsomnia = false;
int? _stressLevel;
final TextEditingController _notesController = TextEditingController();
final TextEditingController _cravingsController = TextEditingController();
// Intimacy tracking
bool _hadIntimacy = false;
bool? _intimacyProtected; // null = no selection, true = protected, false = unprotected
// Hidden field to preserve husband's notes
String? _husbandNotes;
@override
void initState() {
super.initState();
_selectedDate = widget.initialDate ?? DateTime.now();
// Defer data loading to avoid build-time ref.read
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadExistingData();
});
}
void _loadExistingData() {
final entries = ref.read(cycleEntriesProvider);
try {
final entry = entries.firstWhere(
(e) => DateUtils.isSameDay(e.date, _selectedDate),
);
setState(() {
_existingEntryId = entry.id;
_isPeriodDay = entry.isPeriodDay;
_flowIntensity = entry.flowIntensity;
_mood = entry.mood;
_energyLevel = entry.energyLevel;
_crampIntensity = entry.crampIntensity ?? 0;
_hasHeadache = entry.hasHeadache;
_hasBloating = entry.hasBloating;
_hasBreastTenderness = entry.hasBreastTenderness;
_hasFatigue = entry.hasFatigue;
_hasAcne = entry.hasAcne;
_hasLowerBackPain = entry.hasLowerBackPain;
_hasConstipation = entry.hasConstipation;
_hasDiarrhea = entry.hasDiarrhea;
_hasInsomnia = entry.hasInsomnia;
_stressLevel = entry.stressLevel;
_notesController.text = entry.notes ?? '';
_cravingsController.text = entry.cravings?.join(', ') ?? '';
_husbandNotes = entry.husbandNotes;
_hadIntimacy = entry.hadIntimacy;
_intimacyProtected = entry.intimacyProtected;
});
} catch (_) {
// No existing entry for this day
}
}
@override
void dispose() {
_notesController.dispose();
_cravingsController.dispose();
super.dispose();
}
Future<void> _saveEntry() async {
List<String>? cravings;
if (_cravingsController.text.isNotEmpty) {
cravings = _cravingsController.text
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
}
final entry = CycleEntry(
id: _existingEntryId ?? const Uuid().v4(),
date: _selectedDate,
isPeriodDay: _isPeriodDay,
flowIntensity: _isPeriodDay ? _flowIntensity : null,
mood: _mood,
energyLevel: _energyLevel,
crampIntensity: _crampIntensity > 0 ? _crampIntensity : null,
hasHeadache: _hasHeadache,
hasBloating: _hasBloating,
hasBreastTenderness: _hasBreastTenderness,
hasFatigue: _hasFatigue,
hasAcne: _hasAcne,
hasLowerBackPain: _hasLowerBackPain,
hasConstipation: _hasConstipation,
hasDiarrhea: _hasDiarrhea,
hasInsomnia: _hasInsomnia,
stressLevel: _stressLevel,
notes: _notesController.text.isNotEmpty ? _notesController.text : null,
cravings: cravings,
husbandNotes: _husbandNotes,
hadIntimacy: _hadIntimacy,
intimacyProtected: _hadIntimacy ? _intimacyProtected : null,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
if (_existingEntryId != null) {
await ref.read(cycleEntriesProvider.notifier).updateEntry(entry);
} else {
await ref.read(cycleEntriesProvider.notifier).addEntry(entry);
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Entry saved!', style: GoogleFonts.outfit()),
backgroundColor: AppColors.success,
behavior: SnackBarBehavior.floating,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
if (widget.initialDate != null) {
Navigator.pop(context);
} else {
_resetForm();
}
}
}
void _resetForm() {
setState(() {
_existingEntryId = null;
_isPeriodDay = false;
_flowIntensity = null;
_mood = null;
_energyLevel = 3;
_crampIntensity = 0;
_hasHeadache = false;
_hasBloating = false;
_hasBreastTenderness = false;
_hasFatigue = false;
_hasAcne = false;
_hasLowerBackPain = false;
_hasConstipation = false;
_hasDiarrhea = false;
_hasInsomnia = false;
_stressLevel = 1;
_notesController.clear();
_cravingsController.clear();
_husbandNotes = null;
_hadIntimacy = false;
_intimacyProtected = null;
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
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),
),
),
],
),
const SizedBox(height: 24),
// Period Toggle
_buildSectionCard(
context,
title: 'Period',
child: Row(
children: [
Expanded(
child: Text(
'Is today a period day?',
style: GoogleFonts.outfit(
fontSize: 16,
color: theme.colorScheme.onSurface,
),
),
),
Switch(
value: _isPeriodDay,
onChanged: (value) => setState(() => _isPeriodDay = value),
activeColor: AppColors.menstrualPhase,
),
],
),
),
// Flow Intensity (only if period day)
if (_isPeriodDay) ...[
const SizedBox(height: 16),
_buildSectionCard(
context,
title: 'Flow Intensity',
child: 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
.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
border: isSelected
? Border.all(color: AppColors.menstrualPhase)
: Border.all(color: Colors.transparent),
),
child: Column(
children: [
Icon(
Icons.water_drop,
color: isSelected
? AppColors.menstrualPhase
: theme.colorScheme.onSurfaceVariant,
size: 20,
),
const SizedBox(height: 4),
Text(
flow.label,
style: GoogleFonts.outfit(
fontSize: 11,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
color: isSelected
? AppColors.menstrualPhase
: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
),
);
}).toList(),
),
),
],
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,
),
),
],
),
),
);
}).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: (_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),
// Intimacy Tracking (for married users)
_buildSectionCard(
context,
title: 'Intimacy',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
title: Text('Had Intimacy Today', style: GoogleFonts.outfit(fontSize: 14)),
value: _hadIntimacy,
onChanged: (val) => setState(() {
_hadIntimacy = val;
if (!val) _intimacyProtected = null;
}),
activeColor: AppColors.sageGreen,
contentPadding: EdgeInsets.zero,
),
if (_hadIntimacy) ...[
const SizedBox(height: 8),
Text('Protection:', style: GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray)),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => setState(() => _intimacyProtected = true),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: _intimacyProtected == true
? AppColors.sageGreen.withOpacity(0.2)
: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _intimacyProtected == true
? AppColors.sageGreen
: Colors.grey.withOpacity(0.3),
),
),
child: Center(
child: Text(
'Protected',
style: GoogleFonts.outfit(
fontWeight: FontWeight.w500,
color: _intimacyProtected == true
? AppColors.sageGreen
: AppColors.warmGray,
),
),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: GestureDetector(
onTap: () => setState(() => _intimacyProtected = false),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: _intimacyProtected == false
? AppColors.rose.withOpacity(0.15)
: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _intimacyProtected == false
? AppColors.rose
: Colors.grey.withOpacity(0.3),
),
),
child: Center(
child: Text(
'Unprotected',
style: GoogleFonts.outfit(
fontWeight: FontWeight.w500,
color: _intimacyProtected == false
? AppColors.rose
: AppColors.warmGray,
),
),
),
),
),
),
],
),
],
],
),
),
const SizedBox(height: 16),
// Notes
_buildSectionCard(
context,
title: 'Notes',
child: TextField(
controller: _notesController,
maxLines: 3,
decoration: InputDecoration(
hintText: 'Add any notes about how you\'re feeling...',
filled: true,
fillColor: isDark
? theme.colorScheme.surface
: theme.colorScheme.surfaceVariant.withOpacity(0.1),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
style: GoogleFonts.outfit(
fontSize: 14,
color: theme.colorScheme.onSurface,
),
),
),
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,
{required String title, required Widget child}) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.cardTheme.color,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.colorScheme.outline.withOpacity(0.05)),
boxShadow: isDark
? null
: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 12),
child,
],
),
);
}
Widget _buildSymptomChip(BuildContext context, String label, bool isSelected,
ValueChanged<bool> onChanged) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => onChanged(!isSelected),
borderRadius: BorderRadius.circular(20),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: isSelected
? theme.colorScheme.tertiary.withOpacity(isDark ? 0.3 : 0.2)
: theme.colorScheme.surfaceVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(20),
border: isSelected
? Border.all(color: theme.colorScheme.tertiary)
: Border.all(color: Colors.transparent),
),
child: Text(
label,
style: GoogleFonts.outfit(
fontSize: 13,
color: isSelected
? theme.colorScheme.onSurface
: theme.colorScheme.onSurfaceVariant,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
),
),
),
),
);
}
String _formatDate(DateTime date) {
final now = DateTime.now();
if (DateUtils.isSameDay(date, now)) {
return 'Today, ${_getMonth(date.month)} ${date.day}';
}
const days = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday'
];
return '${days[date.weekday - 1]}, ${_getMonth(date.month)} ${date.day}';
}
String _getMonth(int month) {
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
return months[month - 1];
}
String _getEnergyLabel(int? level) {
if (level == null) return 'Not logged';
switch (level) {
case 1:
return 'Very Low';
case 2:
return 'Low';
case 3:
return 'Normal';
case 4:
return 'Good';
case 5:
return 'Excellent';
default:
return 'Normal';
}
}
}

View File

@@ -0,0 +1,790 @@
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 'package:christian_period_tracker/models/user_profile.dart';
import 'package:christian_period_tracker/models/cycle_entry.dart';
import '../home/home_screen.dart';
import '../husband/husband_home_screen.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/user_provider.dart';
class OnboardingScreen extends ConsumerStatefulWidget {
const OnboardingScreen({super.key});
@override
ConsumerState<OnboardingScreen> createState() => _OnboardingScreenState();
}
class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
final PageController _pageController = PageController();
int _currentPage = 0;
bool _isNavigating = false; // Debounce flag
// Form data
UserRole _role = UserRole.wife;
String _name = '';
RelationshipStatus _relationshipStatus = RelationshipStatus.single;
FertilityGoal? _fertilityGoal;
int _averageCycleLength = 28;
DateTime? _lastPeriodStart;
bool _isIrregularCycle = false;
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
void _nextPage() async {
if (_isNavigating) return;
_isNavigating = true;
// Husband Flow: Role (0) -> Name (1) -> Finish
// Wife Flow: Role (0) -> Name (1) -> Relationship (2) -> [Fertility (3)] -> Cycle (4)
int nextPage = _currentPage + 1;
// Logic for skipping pages
if (_role == UserRole.husband) {
if (_currentPage == 1) {
await _completeOnboarding();
return;
}
} else {
// Wife flow
if (_currentPage == 2 &&
_relationshipStatus != RelationshipStatus.married) {
// Skip fertility goal (page 3) if not married
nextPage = 4;
}
}
if (nextPage <= 4) {
// Max pages
await _pageController.animateToPage(
nextPage,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
} else {
await _completeOnboarding();
}
// Reset debounce after animation
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) setState(() => _isNavigating = false);
});
}
void _previousPage() async {
if (_isNavigating) return;
_isNavigating = true;
int prevPage = _currentPage - 1;
// Logic for reverse skipping
if (_role == UserRole.wife) {
if (_currentPage == 4 &&
_relationshipStatus != RelationshipStatus.married) {
// Skip back over fertility goal (page 3)
prevPage = 2;
}
}
if (prevPage >= 0) {
await _pageController.animateToPage(
prevPage,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
}
// Reset debounce after animation
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) setState(() => _isNavigating = false);
});
}
Future<void> _completeOnboarding() async {
final userProfile = UserProfile(
id: const Uuid().v4(),
name: _name,
role: _role,
relationshipStatus: _role == UserRole.husband
? RelationshipStatus.married
: _relationshipStatus,
fertilityGoal: (_role == UserRole.wife &&
_relationshipStatus == RelationshipStatus.married)
? _fertilityGoal
: null,
averageCycleLength: _averageCycleLength,
lastPeriodStartDate: _lastPeriodStart,
isIrregularCycle: _isIrregularCycle,
hasCompletedOnboarding: true,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
await ref.read(userProfileProvider.notifier).updateProfile(userProfile);
if (mounted) {
// Navigate to appropriate home screen
if (_role == UserRole.husband) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const HusbandHomeScreen(),
),
);
} else {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const HomeScreen()),
);
}
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
// Different background color for husband flow
final isHusband = _role == UserRole.husband;
final bgColor = isHusband
? (isDark ? const Color(0xFF1A1C1E) : AppColors.warmCream)
: theme.scaffoldBackgroundColor;
return Scaffold(
backgroundColor: bgColor,
body: SafeArea(
child: Column(
children: [
// Progress indicator (hide on role page 0)
if (_currentPage > 0)
Padding(
padding: const EdgeInsets.all(24),
child: SmoothPageIndicator(
controller: _pageController,
count: isHusband ? 2 : 5,
effect: WormEffect(
dotHeight: 8,
dotWidth: 8,
spacing: 12,
activeDotColor:
isHusband ? AppColors.navyBlue : AppColors.sageGreen,
dotColor: theme.colorScheme.outline.withOpacity(0.2),
),
),
),
// Pages
Expanded(
child: PageView(
controller: _pageController,
physics:
const NeverScrollableScrollPhysics(), // Disable swipe
onPageChanged: (index) {
setState(() => _currentPage = index);
},
children: [
_buildRolePage(), // Page 0
_buildNamePage(), // Page 1
_buildRelationshipPage(), // Page 2 (Wife only)
_buildFertilityGoalPage(), // Page 3 (Wife married only)
_buildCyclePage(), // Page 4 (Wife only)
],
),
),
],
),
),
);
}
Widget _buildRolePage() {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.blushPink,
AppColors.rose.withOpacity(0.7)
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
Icons.favorite_rounded,
size: 40,
color: Colors.white,
),
),
const SizedBox(height: 32),
Text(
'Who is this app for?',
textAlign: TextAlign.center,
style: theme.textTheme.displayMedium?.copyWith(
fontSize: 28,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 48),
_buildRoleOption(UserRole.wife, 'For Her',
'Track cycle, health, and faith', Icons.female),
const SizedBox(height: 16),
_buildRoleOption(UserRole.husband, 'For Him',
'Support your wife and grow together', Icons.male),
const Spacer(),
SizedBox(
width: double.infinity,
height: 54,
child: ElevatedButton(
onPressed: _nextPage,
style: ElevatedButton.styleFrom(
backgroundColor: _role == UserRole.husband
? AppColors.navyBlue
: AppColors.sageGreen,
),
child: const Text('Continue'),
),
),
],
),
);
}
Widget _buildRoleOption(
UserRole role, String title, String subtitle, IconData icon) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final isSelected = _role == role;
// Dynamic colors based on role selection
final activeColor =
role == UserRole.wife ? AppColors.sageGreen : AppColors.navyBlue;
final activeBg = activeColor.withOpacity(isDark ? 0.3 : 0.1);
return GestureDetector(
onTap: () => setState(() => _role = role),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isSelected ? activeBg : theme.cardTheme.color,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? activeColor
: theme.colorScheme.outline.withOpacity(0.1),
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected
? activeColor
: theme.colorScheme.surfaceVariant,
shape: BoxShape.circle,
),
child: Icon(
icon,
color: isSelected
? Colors.white
: theme.colorScheme.onSurfaceVariant,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.titleLarge?.copyWith(
fontSize: 18,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
if (isSelected)
Icon(Icons.check_circle, color: activeColor),
],
),
),
);
}
Widget _buildNamePage() {
final theme = Theme.of(context);
final isHusband = _role == UserRole.husband;
final activeColor =
isHusband ? AppColors.navyBlue : AppColors.sageGreen;
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
Text(
isHusband ? 'What\'s your name, sir?' : 'What\'s your name?',
style: theme.textTheme.displaySmall?.copyWith(
fontSize: 28,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Text(
'We\'ll use this to personalize the app.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 32),
TextField(
onChanged: (value) => setState(() => _name = value),
decoration: InputDecoration(
hintText: 'Enter your name',
prefixIcon: Icon(
Icons.person_outline,
color: theme.colorScheme.onSurfaceVariant,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: activeColor, width: 2),
),
),
style: theme.textTheme.bodyLarge,
textCapitalization: TextCapitalization.words,
),
const Spacer(),
Row(
children: [
Expanded(
child: SizedBox(
height: 54,
child: OutlinedButton(
onPressed: _previousPage,
style: OutlinedButton.styleFrom(
foregroundColor: activeColor,
side: BorderSide(color: activeColor),
),
child: const Text('Back'),
),
),
),
const SizedBox(width: 16),
Expanded(
child: SizedBox(
height: 54,
child: ElevatedButton(
onPressed: (_name.isNotEmpty && !_isNavigating)
? _nextPage
: null,
style: ElevatedButton.styleFrom(
backgroundColor: activeColor,
),
child: Text(isHusband ? 'Finish Setup' : 'Continue'),
),
),
),
],
),
],
),
);
}
Widget _buildRelationshipPage() {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
Text(
'Tell us about yourself',
style: theme.textTheme.displaySmall?.copyWith(
fontSize: 28,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 32),
_buildRelationshipOption(RelationshipStatus.single, 'Single',
'Wellness focus', Icons.person_outline),
const SizedBox(height: 12),
_buildRelationshipOption(RelationshipStatus.engaged, 'Engaged',
'Prepare for marriage', Icons.favorite_border),
const SizedBox(height: 12),
_buildRelationshipOption(RelationshipStatus.married, 'Married',
'Fertility & intimacy', Icons.favorite),
const Spacer(),
Row(
children: [
Expanded(
child: SizedBox(
height: 54,
child: OutlinedButton(
onPressed: _previousPage,
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.sageGreen,
side: const BorderSide(color: AppColors.sageGreen)),
child: const Text('Back'),
),
),
),
const SizedBox(width: 16),
Expanded(
child: SizedBox(
height: 54,
child: ElevatedButton(
onPressed: (_relationshipStatus != null && !_isNavigating)
? _nextPage
: null,
child: const Text('Continue'),
),
),
),
],
),
],
),
);
}
Widget _buildRelationshipOption(
RelationshipStatus status, String title, String subtitle, IconData icon) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final isSelected = _relationshipStatus == status;
return GestureDetector(
onTap: () => setState(() => _relationshipStatus = status),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isSelected
? AppColors.sageGreen.withOpacity(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),
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
Icon(icon,
color: isSelected
? AppColors.sageGreen
: theme.colorScheme.onSurfaceVariant),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface)),
Text(subtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant)),
],
),
),
if (isSelected)
Icon(Icons.check_circle, color: AppColors.sageGreen),
],
),
),
);
}
Widget _buildFertilityGoalPage() {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
Text('What\'s your goal?',
style: theme.textTheme.displaySmall?.copyWith(
fontSize: 28,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface)),
const SizedBox(height: 32),
_buildGoalOption(
FertilityGoal.tryingToConceive,
'Trying to Conceive',
'Track fertile days',
Icons.child_care_outlined),
const SizedBox(height: 12),
_buildGoalOption(
FertilityGoal.tryingToAvoid,
'Natural Family Planning',
'Track fertility signs',
Icons.calendar_today_outlined),
const SizedBox(height: 12),
_buildGoalOption(FertilityGoal.justTracking, 'Just Tracking',
'Monitor cycle health', Icons.insights_outlined),
const Spacer(),
Row(
children: [
Expanded(
child: SizedBox(
height: 54,
child: OutlinedButton(
onPressed: _previousPage,
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.sageGreen,
side: const BorderSide(color: AppColors.sageGreen)),
child: const Text('Back'),
),
),
),
const SizedBox(width: 16),
Expanded(
child: SizedBox(
height: 54,
child: ElevatedButton(
onPressed: (_fertilityGoal != null && !_isNavigating)
? _nextPage
: null,
child: const Text('Continue'),
),
),
),
],
),
],
),
);
}
Widget _buildGoalOption(
FertilityGoal goal, String title, String subtitle, IconData icon) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final isSelected = _fertilityGoal == goal;
return GestureDetector(
onTap: () => setState(() => _fertilityGoal = goal),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isSelected
? AppColors.sageGreen.withOpacity(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),
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
Icon(icon,
color: isSelected
? AppColors.sageGreen
: theme.colorScheme.onSurfaceVariant),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface)),
Text(subtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant)),
],
),
),
if (isSelected)
Icon(Icons.check_circle, color: AppColors.sageGreen),
],
),
),
);
}
Widget _buildCyclePage() {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
Text('About your cycle',
style: theme.textTheme.displaySmall?.copyWith(
fontSize: 28,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface)),
const SizedBox(height: 32),
Text('Average cycle length',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurface)),
Row(
children: [
Expanded(
child: Slider(
value: _averageCycleLength.toDouble(),
min: 21,
max: 40,
divisions: 19,
activeColor: AppColors.sageGreen,
onChanged: (value) =>
setState(() => _averageCycleLength = value.round()),
),
),
Text('$_averageCycleLength days',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: AppColors.sageGreen)),
],
),
// Irregular Cycle Checkbox
CheckboxListTile(
title: Text('My cycles are irregular',
style: theme.textTheme.bodyLarge
?.copyWith(color: theme.colorScheme.onSurface)),
value: _isIrregularCycle,
onChanged: (val) =>
setState(() => _isIrregularCycle = val ?? false),
activeColor: AppColors.sageGreen,
contentPadding: EdgeInsets.zero,
controlAffinity: ListTileControlAffinity.leading,
),
const SizedBox(height: 24),
Text('Last period start date',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurface)),
const SizedBox(height: 8),
GestureDetector(
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: _lastPeriodStart ?? DateTime.now(),
firstDate: DateTime.now().subtract(const Duration(days: 60)),
lastDate: DateTime.now(),
builder: (context, child) {
return Theme(
data: theme.copyWith(
colorScheme: theme.colorScheme.copyWith(
primary: AppColors.sageGreen,
onPrimary: Colors.white,
),
),
child: child!,
);
},
);
if (date != null) setState(() => _lastPeriodStart = date);
},
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.cardTheme.color,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: theme.colorScheme.outline.withOpacity(0.1))),
child: Row(
children: [
Icon(Icons.calendar_today,
color: theme.colorScheme.onSurfaceVariant),
const SizedBox(width: 12),
Text(
_lastPeriodStart != null
? "${_lastPeriodStart!.month}/${_lastPeriodStart!.day}/${_lastPeriodStart!.year}"
: "Select Date",
style: theme.textTheme.bodyLarge
?.copyWith(color: theme.colorScheme.onSurface)),
],
),
),
),
const Spacer(),
Row(
children: [
Expanded(
child: SizedBox(
height: 54,
child: OutlinedButton(
onPressed: _previousPage,
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.sageGreen,
side: const BorderSide(color: AppColors.sageGreen)),
child: const Text('Back'),
),
),
),
const SizedBox(width: 16),
Expanded(
child: SizedBox(
height: 54,
child: ElevatedButton(
onPressed: (_lastPeriodStart != null && !_isNavigating)
? _nextPage
: null,
child: const Text('Get Started'),
),
),
),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,183 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../theme/app_theme.dart';
import 'onboarding/onboarding_screen.dart';
import 'home/home_screen.dart';
import '../models/user_profile.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/user_provider.dart';
import 'husband/husband_home_screen.dart';
class SplashScreen extends ConsumerStatefulWidget {
const SplashScreen({super.key});
@override
ConsumerState<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends ConsumerState<SplashScreen> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
),
);
_scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Curves.easeOutBack),
),
);
_controller.forward();
// Navigate after splash
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();
} else if (user.role == UserRole.husband) {
nextScreen = const HusbandHomeScreen();
} else {
nextScreen = const HomeScreen();
}
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => nextScreen,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 500),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.cream,
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// App Icon/Logo
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.blushPink,
AppColors.rose.withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: AppColors.rose.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: const Icon(
Icons.favorite_rounded,
size: 60,
color: Colors.white,
),
),
const SizedBox(height: 24),
// App Name placeholder
Text(
'Period Tracker',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 8),
// Tagline
Text(
'Faith-Centered Wellness',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.warmGray,
letterSpacing: 1.2,
),
),
const SizedBox(height: 48),
// Scripture
Padding(
padding: const EdgeInsets.symmetric(horizontal: 48),
child: Text(
'"I praise you because I am\nfearfully and wonderfully made."',
textAlign: TextAlign.center,
style: GoogleFonts.lora(
fontSize: 16,
fontStyle: FontStyle.italic,
color: AppColors.charcoal,
height: 1.5,
),
),
),
const SizedBox(height: 8),
Text(
'— Psalm 139:14',
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
],
),
),
);
},
),
),
);
}
}

View File

@@ -0,0 +1,48 @@
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<void> showTranslationPicker(BuildContext context, WidgetRef ref) async {
final user = ref.read(userProfileProvider);
if (user == null) return;
final selected = await showModalBottomSheet<BibleTranslation>(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => Container(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Text(
'Select Bible Translation',
style: Theme.of(context).textTheme.titleLarge,
),
),
...BibleTranslation.values.map((t) => ListTile(
title: Text(t.label),
trailing: user.bibleTranslation == t
? const Icon(Icons.check, color: AppColors.sageGreen)
: null,
onTap: () => Navigator.pop(context, t),
)),
],
),
),
);
if (selected != null) {
await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(bibleTranslation: selected)
);
}
}
}

View File

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

View File

@@ -0,0 +1,96 @@
import '../models/user_profile.dart';
import '../models/cycle_entry.dart';
class CycleInfo {
final CyclePhase phase;
final int dayOfCycle;
final int daysUntilPeriod;
final bool isPeriodExpected;
CycleInfo({
required this.phase,
required this.dayOfCycle,
required this.daysUntilPeriod,
required this.isPeriodExpected,
});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CycleInfo &&
runtimeType == other.runtimeType &&
phase == other.phase &&
dayOfCycle == other.dayOfCycle &&
daysUntilPeriod == other.daysUntilPeriod &&
isPeriodExpected == other.isPeriodExpected;
@override
int get hashCode =>
phase.hashCode ^
dayOfCycle.hashCode ^
daysUntilPeriod.hashCode ^
isPeriodExpected.hashCode;
}
class CycleService {
/// Calculates the current cycle information based on user profile
static CycleInfo calculateCycleInfo(UserProfile? user) {
if (user?.lastPeriodStartDate == null) {
return CycleInfo(
phase: CyclePhase.follicular,
dayOfCycle: 1,
daysUntilPeriod: user?.averageCycleLength ?? 28,
isPeriodExpected: false,
);
}
final lastPeriod = user!.lastPeriodStartDate!;
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 startOfLastPeriod = DateTime(lastPeriod.year, lastPeriod.month, lastPeriod.day);
final daysSinceLastPeriod = startOfToday.difference(startOfLastPeriod).inDays + 1;
// 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 <= 5) {
phase = CyclePhase.menstrual;
} else if (dayOfCycle <= 13) {
phase = CyclePhase.follicular;
} else if (dayOfCycle <= 16) {
phase = CyclePhase.ovulation;
} else {
phase = CyclePhase.luteal;
}
return CycleInfo(
phase: phase,
dayOfCycle: dayOfCycle,
daysUntilPeriod: daysUntilPeriod,
isPeriodExpected: daysUntilPeriod <= 0 || dayOfCycle <= 5,
);
}
/// Format cycle day for display
static String getDayOfCycleDisplay(int day) => 'Day $day';
/// Get phase description
static String getPhaseDescription(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return 'Your body is resting and clearing. Be gentle with yourself.';
case CyclePhase.follicular:
return 'Energy and optimism are rising. A great time for new projects.';
case CyclePhase.ovulation:
return 'You are at your peak energy and fertility.';
case CyclePhase.luteal:
return 'Progesterone is rising. You may feel more introverted or sensitive.';
}
}
}

View File

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

462
lib/theme/app_theme.dart Normal file
View File

@@ -0,0 +1,462 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
/// App color palette for wife's experience
class AppColors {
// Primary Colors (Wife's App)
static const Color blushPink = Color(0xFFF8E1E7);
static const Color rose = Color(0xFFE8A0B0);
static const Color sageGreen = Color(0xFFA8C5A8);
static const Color lavender = Color(0xFFD4C4E8);
static const Color cream = Color(0xFFFDF8F5);
static const Color softGold = Color(0xFFD4A574);
// Text Colors
static const Color charcoal = Color(0xFF3D3D3D);
static const Color warmGray = Color(0xFF7A7A7A);
static const Color lightGray = Color(0xFFB8B8B8);
// Husband's App Colors
static const Color navyBlue = Color(0xFF2C3E50);
static const Color steelBlue = Color(0xFF5D7B93);
static const Color warmCream = Color(0xFFF5F0E8);
static const Color gold = Color(0xFFC9A961);
static const Color softCoral = Color(0xFFE8B4A8);
// Phase Colors
static const Color menstrualPhase = Color(0xFFE88A9E);
static const Color follicularPhase = Color(0xFF8BC5A3);
static const Color ovulationPhase = Color(0xFFB8A5D4);
static const Color lutealPhase = Color(0xFF8BA5C5);
// Semantic Colors
static const Color success = Color(0xFF7AB98A);
static const Color warning = Color(0xFFE8C567);
static const Color error = Color(0xFFE87B7B);
static const Color info = Color(0xFF7BB8E8);
}
/// App theme configuration
class AppTheme {
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
// Color Scheme
colorScheme: const ColorScheme.light(
primary: AppColors.sageGreen,
secondary: AppColors.rose,
tertiary: AppColors.lavender,
surface: AppColors.cream,
error: AppColors.error,
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: AppColors.charcoal,
),
// Scaffold
scaffoldBackgroundColor: AppColors.cream,
// AppBar
appBarTheme: AppBarTheme(
backgroundColor: AppColors.cream,
foregroundColor: AppColors.charcoal,
elevation: 0,
centerTitle: true,
titleTextStyle: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
// Text Theme
textTheme: TextTheme(
displayLarge: GoogleFonts.outfit(
fontSize: 32,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
displayMedium: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
headlineLarge: GoogleFonts.outfit(
fontSize: 24,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
headlineMedium: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.w500,
color: AppColors.charcoal,
),
titleLarge: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w500,
color: AppColors.charcoal,
),
titleMedium: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.charcoal,
),
bodyLarge: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w400,
color: AppColors.charcoal,
),
bodyMedium: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.charcoal,
),
bodySmall: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w400,
color: AppColors.warmGray,
),
labelLarge: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.charcoal,
),
),
// Card Theme
cardTheme: CardThemeData(
color: Colors.white,
elevation: 2,
shadowColor: AppColors.charcoal.withOpacity(0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
// Button Themes
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.sageGreen,
foregroundColor: Colors.white,
elevation: 2,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
textStyle: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.sageGreen,
side: const BorderSide(color: AppColors.sageGreen, width: 1.5),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
textStyle: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: AppColors.rose,
textStyle: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
// Input Decoration
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: AppColors.lightGray.withOpacity(0.5)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: AppColors.lightGray.withOpacity(0.5)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.sageGreen, width: 2),
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
hintStyle: GoogleFonts.outfit(
color: AppColors.lightGray,
fontSize: 14,
),
),
// Bottom Navigation
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: Colors.white,
selectedItemColor: AppColors.sageGreen,
unselectedItemColor: AppColors.warmGray,
type: BottomNavigationBarType.fixed,
elevation: 8,
selectedLabelStyle: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
),
unselectedLabelStyle: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
// Floating Action Button
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: AppColors.sageGreen,
foregroundColor: Colors.white,
elevation: 4,
),
// Slider Theme
sliderTheme: SliderThemeData(
activeTrackColor: AppColors.sageGreen,
inactiveTrackColor: AppColors.lightGray.withOpacity(0.3),
thumbColor: AppColors.sageGreen,
overlayColor: AppColors.sageGreen.withOpacity(0.2),
trackHeight: 4,
),
// Divider
dividerTheme: DividerThemeData(
color: AppColors.lightGray.withOpacity(0.3),
thickness: 1,
space: 24,
),
);
}
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
// Color Scheme
colorScheme: ColorScheme.dark(
primary: AppColors.sageGreen,
secondary: AppColors.rose,
tertiary: AppColors.lavender,
surface: const Color(0xFF1E1E1E),
error: AppColors.error,
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: Colors.white,
onSurfaceVariant: Colors.white70,
outline: Colors.white.withOpacity(0.1),
),
// Scaffold
scaffoldBackgroundColor: const Color(0xFF121212),
// AppBar
appBarTheme: AppBarTheme(
backgroundColor: const Color(0xFF121212),
foregroundColor: Colors.white,
elevation: 0,
centerTitle: true,
titleTextStyle: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
// Text Theme
textTheme: TextTheme(
displayLarge: GoogleFonts.outfit(
fontSize: 32,
fontWeight: FontWeight.w600,
color: Colors.white,
),
displayMedium: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: Colors.white,
),
headlineLarge: GoogleFonts.outfit(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Colors.white,
),
headlineMedium: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.w500,
color: Colors.white,
),
titleLarge: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.white,
),
titleMedium: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white,
),
bodyLarge: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.white,
),
bodyMedium: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Colors.white70,
),
bodySmall: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Colors.white54,
),
labelLarge: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
// Card Theme
cardTheme: CardThemeData(
color: const Color(0xFF1E1E1E),
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)),
),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
// Button Themes
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.sageGreen,
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
textStyle: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.sageGreen,
side: BorderSide(color: AppColors.sageGreen.withOpacity(0.5), width: 1.5),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
textStyle: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
// Input Decoration
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: const Color(0xFF1E1E1E),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.white.withOpacity(0.1)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.white.withOpacity(0.1)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.sageGreen, width: 2),
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
hintStyle: GoogleFonts.outfit(
color: Colors.white38,
fontSize: 14,
),
),
// Bottom Navigation
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: const Color(0xFF1E1E1E),
selectedItemColor: AppColors.sageGreen,
unselectedItemColor: Colors.white38,
type: BottomNavigationBarType.fixed,
elevation: 0,
selectedLabelStyle: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
),
unselectedLabelStyle: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
// Slider Theme
sliderTheme: SliderThemeData(
activeTrackColor: AppColors.sageGreen,
inactiveTrackColor: Colors.white.withOpacity(0.1),
thumbColor: AppColors.sageGreen,
overlayColor: AppColors.sageGreen.withOpacity(0.2),
trackHeight: 4,
tickMarkShape: const RoundSliderTickMarkShape(),
activeTickMarkColor: Colors.white24,
inactiveTickMarkColor: Colors.white10,
),
// Divider
dividerTheme: DividerThemeData(
color: Colors.white.withOpacity(0.05),
thickness: 1,
space: 24,
),
);
}
}
/// Scripture text style
TextStyle scriptureStyle(BuildContext context, {double? fontSize}) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return GoogleFonts.lora(
fontSize: fontSize ?? 16,
fontStyle: FontStyle.italic,
color: isDark ? Colors.white : AppColors.charcoal,
height: 1.6,
);
}
/// Scripture reference style
TextStyle scriptureRefStyle(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: isDark ? Colors.white54 : AppColors.warmGray,
);
}

266
lib/widgets/cycle_ring.dart Normal file
View File

@@ -0,0 +1,266 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math;
import '../theme/app_theme.dart';
import '../models/cycle_entry.dart';
class CycleRing extends StatefulWidget {
final int dayOfCycle;
final int totalDays;
final CyclePhase phase;
const CycleRing({
super.key,
required this.dayOfCycle,
required this.totalDays,
required this.phase,
});
@override
State<CycleRing> createState() => _CycleRingState();
}
class _CycleRingState extends State<CycleRing>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeOutCubic,
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final targetProgress = widget.dayOfCycle / widget.totalDays;
final daysUntilNextPeriod = widget.totalDays - widget.dayOfCycle;
final isDark = Theme.of(context).brightness == Brightness.dark;
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
final currentProgress = targetProgress * _animation.value;
return SizedBox(
width: 220,
height: 220,
child: Stack(
alignment: Alignment.center,
children: [
// Background ring
CustomPaint(
size: const Size(220, 220),
painter: _CycleRingPainter(
progress: currentProgress,
phase: widget.phase,
isDark: isDark,
),
),
// Center content with scale and fade animation
Transform.scale(
scale: 0.8 + (0.2 * _animation.value),
child: Opacity(
opacity: _animation.value,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Day ${widget.dayOfCycle}',
style: Theme.of(context)
.textTheme
.displayMedium
?.copyWith(
fontSize: 32,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getPhaseColor(widget.phase)
.withOpacity(isDark ? 0.3 : 0.2),
borderRadius: BorderRadius.circular(20),
border: isDark
? Border.all(
color: _getPhaseColor(widget.phase)
.withOpacity(0.5))
: null,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.phase.emoji,
style: const TextStyle(fontSize: 14),
),
const SizedBox(width: 6),
Text(
widget.phase.label,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: isDark
? Colors.white
: _getPhaseColor(widget.phase),
),
),
],
),
),
const SizedBox(height: 8),
Text(
daysUntilNextPeriod > 0
? '$daysUntilNextPeriod days until period'
: 'Period expected',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 12,
color:
Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
),
],
),
);
},
);
}
Color _getPhaseColor(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return AppColors.menstrualPhase;
case CyclePhase.follicular:
return AppColors.follicularPhase;
case CyclePhase.ovulation:
return AppColors.ovulationPhase;
case CyclePhase.luteal:
return AppColors.lutealPhase;
}
}
}
class _CycleRingPainter extends CustomPainter {
final double progress;
final CyclePhase phase;
final bool isDark;
_CycleRingPainter({
required this.progress,
required this.phase,
required this.isDark,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 15;
const strokeWidth = 12.0;
// Background arc
final bgPaint = Paint()
..color =
(isDark ? Colors.white : AppColors.lightGray).withOpacity(isDark ? 0.05 : 0.1)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
canvas.drawCircle(center, radius, bgPaint);
// Progress arc
final progressPaint = Paint()
..shader = SweepGradient(
startAngle: -math.pi / 2,
endAngle: math.pi * 1.5,
colors: _getGradientColors(phase),
stops: const [0.0, 0.5, 1.0],
).createShader(Rect.fromCircle(center: center, radius: radius))
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-math.pi / 2,
2 * math.pi * progress,
false,
progressPaint,
);
// Dot at current position
final dotAngle = -math.pi / 2 + 2 * math.pi * progress;
final dotX = center.dx + radius * math.cos(dotAngle);
final dotY = center.dy + radius * math.sin(dotAngle);
final dotPaint = Paint()
..color = isDark ? const Color(0xFF1E1E1E) : Colors.white
..style = PaintingStyle.fill;
final dotBorderPaint = Paint()
..color = _getPhaseColor(phase)
..style = PaintingStyle.stroke
..strokeWidth = 3;
canvas.drawCircle(Offset(dotX, dotY), 8, dotPaint);
canvas.drawCircle(Offset(dotX, dotY), 8, dotBorderPaint);
}
List<Color> _getGradientColors(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return [AppColors.rose, AppColors.menstrualPhase, AppColors.blushPink];
case CyclePhase.follicular:
return [
AppColors.sageGreen,
AppColors.follicularPhase,
AppColors.sageGreen.withOpacity(0.7)
];
case CyclePhase.ovulation:
return [AppColors.lavender, AppColors.ovulationPhase, AppColors.rose];
case CyclePhase.luteal:
return [
AppColors.lutealPhase,
AppColors.lavender,
AppColors.blushPink
];
}
}
Color _getPhaseColor(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return AppColors.menstrualPhase;
case CyclePhase.follicular:
return AppColors.follicularPhase;
case CyclePhase.ovulation:
return AppColors.ovulationPhase;
case CyclePhase.luteal:
return AppColors.lutealPhase;
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

View File

@@ -0,0 +1,97 @@
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 '../providers/navigation_provider.dart';
class QuickLogButtons extends ConsumerWidget {
const QuickLogButtons({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Row(
children: [
_buildQuickButton(
context,
icon: Icons.water_drop_outlined,
label: 'Period',
color: AppColors.menstrualPhase,
onTap: () => _navigateToLog(ref),
),
const SizedBox(width: 12),
_buildQuickButton(
context,
icon: Icons.emoji_emotions_outlined,
label: 'Mood',
color: AppColors.softGold,
onTap: () => _navigateToLog(ref),
),
const SizedBox(width: 12),
_buildQuickButton(
context,
icon: Icons.flash_on_outlined,
label: 'Energy',
color: AppColors.follicularPhase,
onTap: () => _navigateToLog(ref),
),
const SizedBox(width: 12),
_buildQuickButton(
context,
icon: Icons.healing_outlined,
label: 'Symptoms',
color: AppColors.lavender,
onTap: () => _navigateToLog(ref),
),
],
);
}
void _navigateToLog(WidgetRef ref) {
// Navigate to the Log tab (index 2)
ref.read(navigationProvider.notifier).setIndex(2);
}
Widget _buildQuickButton(
BuildContext context, {
required IconData icon,
required String label,
required Color color,
required VoidCallback onTap,
}) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Expanded(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: color.withOpacity(isDark ? 0.2 : 0.15),
borderRadius: BorderRadius.circular(12),
border:
isDark ? Border.all(color: color.withOpacity(0.3)) : null,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 24),
const SizedBox(height: 6),
Text(
label,
style: GoogleFonts.outfit(
fontSize: 11,
fontWeight: FontWeight.w600,
color: isDark ? Colors.white.withOpacity(0.9) : color,
),
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,193 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../theme/app_theme.dart';
import '../models/cycle_entry.dart';
class ScriptureCard extends StatelessWidget {
final String verse;
final String reference;
final String? translation;
final CyclePhase phase;
final VoidCallback? onTranslationTap;
const ScriptureCard({
super.key,
required this.verse,
required this.reference,
this.translation,
required this.phase,
this.onTranslationTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: _getGradientColors(context, phase),
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isDark
? Colors.white.withOpacity(0.05)
: Colors.black.withOpacity(0.05)),
boxShadow: [
BoxShadow(
color: _getPhaseColor(phase).withOpacity(isDark ? 0.05 : 0.15),
blurRadius: 15,
offset: const Offset(0, 8),
),
],
),
child: Material(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Scripture icon
Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: (isDark ? Colors.white : Colors.black)
.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.menu_book_outlined,
size: 18,
color: isDark
? Colors.white70
: AppColors.charcoal.withOpacity(0.8),
),
),
const SizedBox(width: 8),
Text(
'Today\'s Verse',
style: theme.textTheme.labelLarge?.copyWith(
fontSize: 12,
color: isDark
? Colors.white60
: AppColors.charcoal.withOpacity(0.7),
letterSpacing: 0.5,
),
),
],
),
const SizedBox(height: 16),
// Verse
Text(
'"$verse"',
style: scriptureStyle(context, fontSize: 17),
),
const SizedBox(height: 12),
// Reference & Translation
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'$reference',
style: scriptureRefStyle(context)
.copyWith(fontSize: 13, fontWeight: FontWeight.w600),
),
if (translation != null)
InkWell(
onTap: onTranslationTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: (isDark ? Colors.white : Colors.black)
.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: (isDark ? Colors.white : Colors.black)
.withOpacity(0.1),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
translation!,
style: scriptureRefStyle(context).copyWith(
fontSize: 10,
fontWeight: FontWeight.w800,
letterSpacing: 0.8,
),
),
const SizedBox(width: 4),
Icon(
Icons.swap_horiz,
size: 14,
color: isDark
? Colors.white38
: AppColors.warmGray,
),
],
),
),
),
],
),
],
),
),
),
);
}
List<Color> _getGradientColors(BuildContext context, CyclePhase phase) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final baseColor = isDark ? const Color(0xFF1E1E1E) : AppColors.cream;
switch (phase) {
case CyclePhase.menstrual:
return [
AppColors.menstrualPhase.withOpacity(isDark ? 0.15 : 0.6),
baseColor,
];
case CyclePhase.follicular:
return [
AppColors.follicularPhase.withOpacity(isDark ? 0.15 : 0.3),
baseColor,
];
case CyclePhase.ovulation:
return [
AppColors.ovulationPhase.withOpacity(isDark ? 0.15 : 0.5),
baseColor,
];
case CyclePhase.luteal:
return [
AppColors.lutealPhase.withOpacity(isDark ? 0.15 : 0.3),
baseColor,
];
}
}
Color _getPhaseColor(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return AppColors.menstrualPhase;
case CyclePhase.follicular:
return AppColors.follicularPhase;
case CyclePhase.ovulation:
return AppColors.ovulationPhase;
case CyclePhase.luteal:
return AppColors.lutealPhase;
}
}
}

Some files were not shown because too many files have changed in this diff Show More