Initial commit: Fixes for linting and compilation

This commit is contained in:
2025-12-20 03:13:55 +00:00
commit 5d746d694e
148 changed files with 11207 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,5 @@
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

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"

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.
}
}

49
lib/main.dart Normal file
View File

@@ -0,0 +1,49 @@
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 '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());
// Open boxes
await Hive.openBox<UserProfile>('user_profile');
await Hive.openBox<CycleEntry>('cycle_entries');
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(),
);
}
}

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

@@ -0,0 +1,335 @@
import 'package:hive/hive.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(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;
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.basalBodyTemperature,
this.cervicalMucus,
this.ovulationTestPositive,
this.notes,
this.sleepHours,
this.waterIntake,
this.hadExercise = false,
this.hadIntimacy = false,
required this.createdAt,
required this.updatedAt,
});
/// Check if any symptoms are logged
bool get hasSymptoms =>
hasHeadache ||
hasBloating ||
hasBreastTenderness ||
hasFatigue ||
hasAcne ||
(crampIntensity != null && crampIntensity! > 0);
/// Check if NFP data is logged
bool get hasNFPData =>
basalBodyTemperature != null ||
cervicalMucus != null ||
ovulationTestPositive != null;
/// Get symptom count
int get symptomCount {
int count = 0;
if (hasHeadache) count++;
if (hasBloating) count++;
if (hasBreastTenderness) count++;
if (hasFatigue) count++;
if (hasAcne) count++;
if (crampIntensity != null && crampIntensity! > 0) count++;
return count;
}
/// 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,
double? basalBodyTemperature,
CervicalMucusType? cervicalMucus,
bool? ovulationTestPositive,
String? notes,
int? sleepHours,
int? waterIntake,
bool? hadExercise,
bool? hadIntimacy,
DateTime? createdAt,
DateTime? updatedAt,
}) {
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,
basalBodyTemperature: basalBodyTemperature ?? this.basalBodyTemperature,
cervicalMucus: cervicalMucus ?? this.cervicalMucus,
ovulationTestPositive: ovulationTestPositive ?? this.ovulationTestPositive,
notes: notes ?? this.notes,
sleepHours: sleepHours ?? this.sleepHours,
waterIntake: waterIntake ?? this.waterIntake,
hadExercise: hadExercise ?? this.hadExercise,
hadIntimacy: hadIntimacy ?? this.hadIntimacy,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? DateTime.now(),
);
}
}
/// 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 '🌙';
}
}
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,310 @@
// 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,
basalBodyTemperature: fields[12] as double?,
cervicalMucus: fields[13] as CervicalMucusType?,
ovulationTestPositive: fields[14] as bool?,
notes: fields[15] as String?,
sleepHours: fields[16] as int?,
waterIntake: fields[17] as int?,
hadExercise: fields[18] as bool,
hadIntimacy: fields[19] as bool,
createdAt: fields[20] as DateTime,
updatedAt: fields[21] as DateTime,
);
}
@override
void write(BinaryWriter writer, CycleEntry obj) {
writer
..writeByte(22)
..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(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);
}
@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;
}

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

@@ -0,0 +1,235 @@
/// Scripture model for daily verses and devotionals
class Scripture {
final String verse;
final String reference;
final String? reflection;
final List<String> applicablePhases;
final List<String> applicableContexts;
const Scripture({
required this.verse,
required this.reference,
this.reflection,
this.applicablePhases = const [],
this.applicableContexts = const [],
});
}
/// Pre-defined scriptures for the app
class ScriptureDatabase {
/// Scriptures for menstrual phase (rest, comfort)
static const List<Scripture> menstrualScriptures = [
Scripture(
verse: "Come to me, all you who are weary and burdened, 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'],
),
Scripture(
verse: "He gives strength to the weary and increases the power of the weak.",
reference: "Isaiah 40:29",
applicablePhases: ['menstrual'],
),
Scripture(
verse: "The Lord is my shepherd; I shall not want. He makes me lie down in green pastures.",
reference: "Psalm 23:1-2",
applicablePhases: ['menstrual'],
),
Scripture(
verse: "Be still, and know that I am God.",
reference: "Psalm 46:10",
reflection: "Use this time of slowing down to be present with God.",
applicablePhases: ['menstrual'],
),
Scripture(
verse: "My grace is sufficient for you, for my power is made perfect in weakness.",
reference: "2 Corinthians 12:9",
applicablePhases: ['menstrual'],
),
];
/// Scriptures for follicular phase (renewal, strength)
static const List<Scripture> follicularScriptures = [
Scripture(
verse: "She is clothed with strength and dignity; she can laugh at the days to come.",
reference: "Proverbs 31:25",
reflection: "You're entering a season of renewed energy. Use it for His glory.",
applicablePhases: ['follicular'],
),
Scripture(
verse: "I can do all this through him who gives me strength.",
reference: "Philippians 4:13",
applicablePhases: ['follicular'],
),
Scripture(
verse: "But those who hope in the Lord will renew their strength. They will soar on wings like eagles.",
reference: "Isaiah 40:31",
applicablePhases: ['follicular'],
),
Scripture(
verse: "This is the day the Lord has made; let us rejoice and be glad in it.",
reference: "Psalm 118:24",
applicablePhases: ['follicular'],
),
Scripture(
verse: "The Lord your God is with you, the Mighty Warrior who saves.",
reference: "Zephaniah 3:17",
applicablePhases: ['follicular'],
),
];
/// Scriptures for ovulation phase (creation, beauty)
static const List<Scripture> ovulationScriptures = [
Scripture(
verse: "For you created my inmost being; you knit me together in my mother's womb. I praise you because I am fearfully and wonderfully made.",
reference: "Psalm 139:13-14",
reflection: "Your body reflects the incredible creativity of God.",
applicablePhases: ['ovulation'],
),
Scripture(
verse: "Children are a heritage from the Lord, offspring a reward from him.",
reference: "Psalm 127:3",
applicablePhases: ['ovulation'],
),
Scripture(
verse: "See, I am doing a new thing! Now it springs up; do you not perceive it?",
reference: "Isaiah 43:19",
applicablePhases: ['ovulation'],
),
Scripture(
verse: "Every good and perfect gift is from above.",
reference: "James 1:17",
applicablePhases: ['ovulation'],
),
];
/// Scriptures for luteal phase / TWW (patience, trust)
static const List<Scripture> lutealScriptures = [
Scripture(
verse: "For I know the plans I have for you, declares the Lord, plans to prosper you and not to harm you, plans to give you hope and a future.",
reference: "Jeremiah 29:11",
reflection: "Whatever this season holds, God's plans for you are good.",
applicablePhases: ['luteal'],
),
Scripture(
verse: "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'],
),
Scripture(
verse: "Trust in the Lord with all your heart and lean not on your own understanding.",
reference: "Proverbs 3:5",
applicablePhases: ['luteal'],
),
Scripture(
verse: "The Lord is close to the brokenhearted and saves those who are crushed in spirit.",
reference: "Psalm 34:18",
applicablePhases: ['luteal'],
),
Scripture(
verse: "And the peace of God, which transcends all understanding, will guard your hearts and your minds in Christ Jesus.",
reference: "Philippians 4:7",
applicablePhases: ['luteal'],
),
Scripture(
verse: "Wait for the Lord; be strong and take heart and wait for the Lord.",
reference: "Psalm 27:14",
applicablePhases: ['luteal'],
),
];
/// Scriptures for husbands
static const List<Scripture> husbandScriptures = [
Scripture(
verse: "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.",
),
Scripture(
verse: "Husbands, in the same way be considerate as you live with your wives, and treat them with respect.",
reference: "1 Peter 3:7",
),
Scripture(
verse: "Two are better than one, because they have a good return for their labor.",
reference: "Ecclesiastes 4:9",
),
Scripture(
verse: "Be completely humble and gentle; be patient, bearing with one another in love.",
reference: "Ephesians 4:2",
),
Scripture(
verse: "Above all, love each other deeply, because love covers over a multitude of sins.",
reference: "1 Peter 4:8",
),
Scripture(
verse: "A husband should fulfill his duty to his wife.",
reference: "1 Corinthians 7:3",
),
Scripture(
verse: "He who finds a wife finds what is good and receives favor from the Lord.",
reference: "Proverbs 18:22",
),
];
/// General womanhood scriptures
static const List<Scripture> womanhoodScriptures = [
Scripture(
verse: "Charm is deceptive, and beauty is fleeting; but a woman who fears the Lord is to be praised.",
reference: "Proverbs 31:30",
),
Scripture(
verse: "She opens her mouth with wisdom, and the teaching of kindness is on her tongue.",
reference: "Proverbs 31:26",
),
Scripture(
verse: "Your beauty should not come from outward adornment... Rather, it should be that of your inner self, the unfading beauty of a gentle and quiet spirit.",
reference: "1 Peter 3:3-4",
),
Scripture(
verse: "God is within her, she will not fall; God will help her at break of day.",
reference: "Psalm 46:5",
),
];
/// Get scripture for current phase
static Scripture getScriptureForPhase(String phase) {
final 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;
default:
scriptures = [...menstrualScriptures, ...follicularScriptures, ...ovulationScriptures, ...lutealScriptures];
}
// Return a scripture based on the day of year for variety
final dayOfYear = DateTime.now().difference(DateTime(DateTime.now().year, 1, 1)).inDays;
return scriptures[dayOfYear % scriptures.length];
}
/// Get scripture for husband
static Scripture getHusbandScripture() {
final dayOfYear = DateTime.now().difference(DateTime(DateTime.now().year, 1, 1)).inDays;
return husbandScriptures[dayOfYear % husbandScriptures.length];
}
/// Get all scriptures
static List<Scripture> getAllScriptures() {
return [
...menstrualScriptures,
...follicularScriptures,
...ovulationScriptures,
...lutealScriptures,
...womanhoodScriptures,
];
}
}

View File

@@ -0,0 +1,163 @@
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,
}
/// 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;
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,
});
/// 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,
}) {
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,
);
}
}
@HiveType(typeId: 8)
enum UserRole {
@HiveField(0)
wife,
@HiveField(1)
husband,
}

View File

@@ -0,0 +1,210 @@
// 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,
);
}
@override
void write(BinaryWriter writer, UserProfile obj) {
writer
..writeByte(15)
..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);
}
@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 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,75 @@
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> 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,412 @@
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';
class CalendarScreen extends ConsumerStatefulWidget {
const CalendarScreen({super.key});
@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) {
// Check if it's a logged period day
final isLoggedPeriod = _isLoggedPeriodDay(date, entries);
if (isLoggedPeriod) {
return Positioned(
bottom: 1,
child: Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: AppColors.menstrualPhase,
shape: BoxShape.circle,
),
),
);
}
final phase = _getPhaseForDate(date, lastPeriodStart, cycleLength);
if (phase != null) {
return Positioned(
bottom: 1,
child: Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.5),
shape: BoxShape.circle,
),
),
);
}
return null;
},
),
),
),
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);
final isLoggedPeriod = entry?.isPeriodDay ?? false;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${_getMonthName(date.month)} ${date.day}, ${date.year}',
style: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 12),
if (phase != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(phase.emoji),
const SizedBox(width: 6),
Text(
phase.label,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: _getPhaseColor(phase),
),
),
],
),
),
const SizedBox(height: 12),
if (isLoggedPeriod)
Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.menstrualPhase.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.menstrualPhase.withOpacity(0.3)),
),
child: Row(
children: [
Icon(Icons.water_drop, color: AppColors.menstrualPhase, size: 20),
const SizedBox(width: 8),
Text(
'Period Recorded',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.menstrualPhase,
),
),
],
),
),
Text(
phase?.description ?? 'No cycle data for this date',
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
],
),
);
}
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,372 @@
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';
class DevotionalScreen extends ConsumerWidget {
const DevotionalScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProfileProvider);
final cycleInfo = ref.watch(currentCycleInfoProvider);
final phase = cycleInfo['phase'] as CyclePhase;
final scripture = ScriptureDatabase.getScriptureForPhase(phase.name);
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
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
_getPhaseColor(phase).withOpacity(0.15),
AppColors.cream,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: _getPhaseColor(phase).withOpacity(0.3),
),
),
child: Column(
children: [
// Quote icon
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.format_quote,
color: _getPhaseColor(phase),
size: 24,
),
),
const SizedBox(height: 20),
// Verse
Text(
'"${scripture.verse}"',
textAlign: TextAlign.center,
style: GoogleFonts.lora(
fontSize: 20,
fontStyle: FontStyle.italic,
color: AppColors.charcoal,
height: 1.6,
),
),
const SizedBox(height: 16),
// Reference
Text(
'${scripture.reference}',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.warmGray,
),
),
],
),
),
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,406 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hive_flutter/hive_flutter.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 '../../services/cycle_service.dart';
class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({super.key});
@override
ConsumerState<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends ConsumerState<HomeScreen> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: [
const _DashboardTab(),
const CalendarScreen(),
const LogScreen(),
const DevotionalScreen(),
_SettingsTab(onReset: () => setState(() => _selectedIndex = 0)),
],
),
bottomNavigationBar: Container(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) => setState(() => _selectedIndex = 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 ConsumerWidget {
const _DashboardTab();
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProfileProvider);
final cycleInfo = ref.watch(currentCycleInfoProvider);
final name = user?.name ?? 'Friend';
final phase = cycleInfo['phase'] as CyclePhase;
final dayOfCycle = cycleInfo['dayOfCycle'] as int;
final cycleLength = user?.averageCycleLength ?? 28;
// Get scripture for current phase
final scripture = ScriptureDatabase.getScriptureForPhase(phase.name);
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Greeting
_buildGreeting(name),
const SizedBox(height: 24),
// Cycle Ring
Center(
child: CycleRing(
dayOfCycle: dayOfCycle,
totalDays: cycleLength,
phase: phase,
),
),
const SizedBox(height: 24),
// Scripture Card
ScriptureCard(
verse: scripture.verse,
reference: scripture.reference,
phase: phase,
),
const SizedBox(height: 20),
// Quick Log Buttons
Text(
'Quick Log',
style: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 12),
const QuickLogButtons(),
const SizedBox(height: 20),
// Today's Tip - Only show if not just tracking or husband (though husband has own screen)
if (user?.role == UserRole.wife)
TipCard(phase: phase, isMarried: user?.isMarried ?? false),
const SizedBox(height: 20),
],
),
),
);
}
Widget _buildGreeting(String name) {
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: AppColors.warmGray,
),
),
Text(
name,
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
],
),
),
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColors.blushPink,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.notifications_outlined,
color: AppColors.rose,
),
),
],
);
}
// Placeholder _calculateCycleInfo removed as it's now in CycleService
}
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: AppColors.charcoal),
title: Text(
title,
style: GoogleFonts.outfit(
fontSize: 16,
color: AppColors.charcoal,
),
),
trailing: 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 user = ref.watch(userProfileProvider);
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Settings',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 24),
// Profile Card
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
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(
user?.name.isNotEmpty == true ? user!.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(
user?.name ?? 'Guest',
style: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
Text(
user?.role == UserRole.husband
? 'HUSBAND'
: (user?.relationshipStatus.name.toUpperCase() ?? 'SINGLE'),
style: GoogleFonts.outfit(
fontSize: 12,
letterSpacing: 1,
color: AppColors.warmGray,
),
),
],
),
),
Icon(Icons.chevron_right, color: AppColors.warmGray),
],
),
),
const SizedBox(height: 24),
// Settings Groups
_buildSettingsGroup('Preferences', [
_buildSettingsTile(context, Icons.notifications_outlined, 'Notifications'),
_buildSettingsTile(context, Icons.palette_outlined, 'Appearance'),
_buildSettingsTile(context, Icons.lock_outline, 'Privacy'),
]),
const SizedBox(height: 16),
_buildSettingsGroup('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('Account', [
_buildSettingsTile(
context,
Icons.logout,
'Reset App / Logout',
onTap: () => _resetApp(context, ref)
),
]),
const SizedBox(height: 16),
],
),
),
);
}
Widget _buildSettingsGroup(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: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: tiles,
),
),
],
);
}
}

View File

@@ -0,0 +1,831 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../theme/app_theme.dart';
import '../../models/cycle_entry.dart';
import '../../models/scripture.dart';
import '../../providers/user_provider.dart';
import '../../services/cycle_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// Husband's companion app main screen
class HusbandHomeScreen extends ConsumerStatefulWidget {
const HusbandHomeScreen({super.key});
@override
ConsumerState<HusbandHomeScreen> createState() => _HusbandHomeScreenState();
}
class _HusbandHomeScreenState extends ConsumerState<HusbandHomeScreen> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Theme(
data: _husbandTheme,
child: Scaffold(
backgroundColor: AppColors.warmCream,
body: IndexedStack(
index: _selectedIndex,
children: [
const _HusbandDashboard(),
const _HusbandTipsScreen(),
const _HusbandLearnScreen(),
const _HusbandSettingsScreen(),
],
),
bottomNavigationBar: Container(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: AppColors.navyBlue.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) => setState(() => _selectedIndex = index),
backgroundColor: Colors.white,
selectedItemColor: AppColors.navyBlue,
unselectedItemColor: AppColors.warmGray,
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.lightbulb_outline),
activeIcon: Icon(Icons.lightbulb),
label: 'Tips',
),
BottomNavigationBarItem(
icon: Icon(Icons.school_outlined),
activeIcon: Icon(Icons.school),
label: 'Learn',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
activeIcon: Icon(Icons.settings),
label: 'Settings',
),
],
),
),
),
);
}
ThemeData get _husbandTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
scaffoldBackgroundColor: AppColors.warmCream,
colorScheme: const ColorScheme.light(
primary: AppColors.navyBlue,
secondary: AppColors.gold,
surface: AppColors.warmCream,
),
appBarTheme: AppBarTheme(
backgroundColor: AppColors.warmCream,
foregroundColor: AppColors.navyBlue,
elevation: 0,
titleTextStyle: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.navyBlue,
foregroundColor: Colors.white,
),
),
);
}
}
class _HusbandDashboard extends ConsumerWidget {
const _HusbandDashboard();
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProfileProvider);
final cycleInfo = ref.watch(currentCycleInfoProvider);
final wifeName = user?.partnerName ?? "Wife";
final phase = cycleInfo['phase'] as CyclePhase;
final dayOfCycle = cycleInfo['dayOfCycle'] as int;
final daysUntilPeriod = cycleInfo['daysUntilPeriod'] as int;
final scripture = ScriptureDatabase.getHusbandScripture();
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Greeting
Text(
'Hey there,',
style: GoogleFonts.outfit(
fontSize: 16,
color: AppColors.warmGray,
),
),
Text(
'Husband',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
const SizedBox(height: 24),
// Wife's Cycle Status
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.navyBlue,
AppColors.steelBlue,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.favorite,
color: Colors.white,
size: 22,
),
),
const SizedBox(width: 12),
Text(
'$wifeName\'s Cycle',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white.withOpacity(0.9),
),
),
],
),
const SizedBox(height: 16),
Text(
'Day $dayOfCycle${phase.label}',
style: GoogleFonts.outfit(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
daysUntilPeriod > 0
? '~$daysUntilPeriod days until period'
: 'Period expected soon',
style: GoogleFonts.outfit(
fontSize: 14,
color: Colors.white.withOpacity(0.8),
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(phase.emoji),
const SizedBox(width: 6),
Text(
_getPhaseHint(phase),
style: GoogleFonts.outfit(
fontSize: 12,
color: Colors.white,
),
),
],
),
),
],
),
),
const SizedBox(height: 20),
// Support Tip
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.navyBlue.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppColors.gold.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.lightbulb_outline,
color: AppColors.gold,
size: 20,
),
),
const SizedBox(width: 12),
Text(
'How to Support Her',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
],
),
const SizedBox(height: 12),
Text(
_getSupportTip(phase),
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.charcoal,
height: 1.5,
),
),
],
),
),
const SizedBox(height: 20),
// Scripture for Husbands
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.gold.withOpacity(0.15),
AppColors.warmCream,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.gold.withOpacity(0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.menu_book,
color: AppColors.gold,
size: 20,
),
const SizedBox(width: 8),
Text(
'Scripture for Husbands',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
],
),
const SizedBox(height: 12),
Text(
'"${scripture.verse}"',
style: GoogleFonts.lora(
fontSize: 15,
fontStyle: FontStyle.italic,
color: AppColors.navyBlue,
height: 1.6,
),
),
const SizedBox(height: 8),
Text(
'${scripture.reference}',
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
],
),
),
const SizedBox(height: 20),
// Prayer Button
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () => _showPrayerPrompt(context, phase),
icon: const Text('🙏', style: TextStyle(fontSize: 18)),
label: Text(
'Pray for ${wifeName}',
style: GoogleFonts.outfit(fontWeight: FontWeight.w500),
),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.navyBlue,
side: const BorderSide(color: AppColors.navyBlue),
padding: const EdgeInsets.symmetric(vertical: 14),
),
),
),
const SizedBox(height: 40),
],
),
),
);
}
String _getPhaseHint(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return 'She may need extra rest';
case CyclePhase.follicular:
return 'Energy is returning';
case CyclePhase.ovulation:
return 'Fertile window';
case CyclePhase.luteal:
return 'PMS may occur';
}
}
String _getSupportTip(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return 'This is a time when she needs extra care. Help with household tasks without being asked. '
'Bring her favorite warm drink, suggest low-key activities, and be extra patient.';
case CyclePhase.follicular:
return 'Her energy is returning! This is a great time to plan dates, work on projects together, '
'and affirm her strengths. She may be more talkative and social.';
case CyclePhase.ovulation:
return 'Prioritize connection time. Romance and quality time matter. '
'If you\'re trying to conceive, this is your fertile window.';
case CyclePhase.luteal:
return 'Be patient—PMS may affect her mood. Listen more, "fix" less. '
'Take initiative on responsibilities and surprise her with comfort foods.';
}
}
void _showPrayerPrompt(BuildContext context, CyclePhase phase) {
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,
children: [
Text(
'🙏 Prayer for Your Wife',
style: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
const SizedBox(height: 20),
Text(
_getPrayer(phase),
textAlign: TextAlign.center,
style: GoogleFonts.lora(
fontSize: 16,
fontStyle: FontStyle.italic,
color: AppColors.charcoal,
height: 1.6,
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.navyBlue,
foregroundColor: Colors.white,
),
child: const Text('Amen'),
),
),
const SizedBox(height: 16),
],
),
),
);
}
String _getPrayer(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return '"Lord, I lift up my wife during this time of rest. '
'Give her body the renewal it needs and grant her Your peace. '
'Help me to serve her with patience and love. Amen."';
case CyclePhase.follicular:
return '"Father, thank You for my wife\'s renewed energy. '
'Bless her endeavors and help me to encourage and support her. '
'May our partnership glorify You. Amen."';
case CyclePhase.ovulation:
return '"Creator God, You have designed my wife fearfully and wonderfully. '
'Whatever Your plans for our family, help us trust Your timing. '
'Bless our marriage and intimacy. Amen."';
case CyclePhase.luteal:
return '"Lord, be near to my wife during this phase. '
'When emotions are difficult, grant her Your peace that passes understanding. '
'Help me to be patient, kind, and understanding. Amen."';
}
}
}
class _HusbandTipsScreen extends StatelessWidget {
const _HusbandTipsScreen();
@override
Widget build(BuildContext context) {
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Supporting Her',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
const SizedBox(height: 8),
Text(
'Practical ways to love your wife well',
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
const SizedBox(height: 24),
_buildTipCategory('During Her Period', [
'🏠 Help with household tasks without being asked',
'🍵 Bring her favorite comfort drink',
'📺 Suggest low-key activities (movies, quiet time)',
'🙏 Pray for her physical comfort',
]),
const SizedBox(height: 16),
_buildTipCategory('Follicular Phase', [
'🎉 Plan dates or activities—her energy is returning',
'💬 She may be more talkative and social',
'💪 Great time for projects together',
'❤️ Affirm her strengths and beauty',
]),
const SizedBox(height: 16),
_buildTipCategory('Luteal Phase (PMS)', [
'😌 Be patient—PMS may affect her mood',
'🍫 Surprise with comfort foods',
'🧹 Take initiative on responsibilities',
'👂 Listen more, "fix" less',
]),
const SizedBox(height: 16),
_buildTipCategory('General Wisdom', [
'🗣️ Ask how she\'s feeling—and actually listen',
'📱 Put your phone down when she\'s talking',
'🌹 Small gestures matter more than grand ones',
'🙏 Pray for her daily',
]),
],
),
),
);
}
Widget _buildTipCategory(String title, List<String> tips) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
const SizedBox(height: 12),
...tips.map((tip) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
tip,
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.charcoal,
height: 1.4,
),
),
)),
],
),
);
}
}
class _HusbandLearnScreen extends StatelessWidget {
const _HusbandLearnScreen();
@override
Widget build(BuildContext context) {
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Learn',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
const SizedBox(height: 24),
_buildSection('Understanding Her', [
_LearnItem(
icon: Icons.loop,
title: 'The 4 Phases of Her Cycle',
subtitle: 'What\'s happening in her body each month',
),
_LearnItem(
icon: Icons.psychology_outlined,
title: 'Why Does Her Mood Change?',
subtitle: 'Hormones explained simply',
),
_LearnItem(
icon: Icons.medical_information_outlined,
title: 'PMS is Real',
subtitle: 'Medical facts for supportive husbands',
),
]),
const SizedBox(height: 24),
_buildSection('Biblical Manhood', [
_LearnItem(
icon: Icons.favorite,
title: 'Loving Like Christ',
subtitle: 'Ephesians 5 in daily practice',
),
_LearnItem(
icon: Icons.handshake,
title: 'Servant Leadership at Home',
subtitle: 'What it really means',
),
_LearnItem(
icon: Icons.auto_awesome,
title: 'Praying for Your Wife',
subtitle: 'Practical guide',
),
]),
const SizedBox(height: 24),
_buildSection('NFP for Husbands', [
_LearnItem(
icon: Icons.show_chart,
title: 'Reading the Charts Together',
subtitle: 'Understanding fertility signs',
),
_LearnItem(
icon: Icons.schedule,
title: 'Abstinence as Spiritual Discipline',
subtitle: 'Growing together during fertile days',
),
]),
],
),
),
);
}
Widget _buildSection(String title, List<_LearnItem> items) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
letterSpacing: 0.5,
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: items
.map((item) => ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.navyBlue.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
item.icon,
color: AppColors.navyBlue,
size: 20,
),
),
title: Text(
item.title,
style: GoogleFonts.outfit(
fontSize: 15,
fontWeight: FontWeight.w500,
color: AppColors.charcoal,
),
),
subtitle: Text(
item.subtitle,
style: GoogleFonts.outfit(
fontSize: 13,
color: AppColors.warmGray,
),
),
trailing: const Icon(
Icons.chevron_right,
color: AppColors.lightGray,
),
onTap: () {},
))
.toList(),
),
),
],
);
}
}
class _LearnItem {
final IconData icon;
final String title;
final String subtitle;
const _LearnItem({
required this.icon,
required this.title,
required this.subtitle,
});
}
class _HusbandSettingsScreen extends ConsumerWidget {
const _HusbandSettingsScreen();
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) {
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
}
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Settings',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
const SizedBox(height: 24),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
ListTile(
leading: const Icon(Icons.notifications_outlined),
title: Text('Notifications', style: GoogleFonts.outfit()),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
ListTile(
leading: const Icon(Icons.link_outlined),
title: Text('Connection', style: GoogleFonts.outfit()),
subtitle: Text('Linked with wife\'s app', style: GoogleFonts.outfit(fontSize: 12)),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
ListTile(
leading: const Icon(Icons.logout),
title: Text('Reset App / Logout', style: GoogleFonts.outfit()),
trailing: const Icon(Icons.chevron_right),
onTap: () => _resetApp(context, ref),
),
ListTile(
leading: const Icon(Icons.help_outline),
title: Text('Help & Support', style: GoogleFonts.outfit()),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,450 @@
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/user_provider.dart';
import '../../theme/app_theme.dart';
import 'package:uuid/uuid.dart';
class LogScreen extends ConsumerStatefulWidget {
const LogScreen({super.key});
@override
ConsumerState<LogScreen> createState() => _LogScreenState();
}
class _LogScreenState extends ConsumerState<LogScreen> {
bool _isPeriodDay = false;
FlowIntensity? _flowIntensity;
MoodLevel? _mood;
int _energyLevel = 3;
int _crampIntensity = 0;
bool _hasHeadache = false;
bool _hasBloating = false;
bool _hasBreastTenderness = false;
bool _hasFatigue = false;
bool _hasAcne = false;
final TextEditingController _notesController = TextEditingController();
@override
void dispose() {
_notesController.dispose();
super.dispose();
}
Future<void> _saveEntry() async {
final entry = CycleEntry(
id: const Uuid().v4(),
date: DateTime.now(),
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,
notes: _notesController.text.isNotEmpty ? _notesController.text : null,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
await ref.read(cycleEntriesProvider.notifier).addEntry(entry);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Entry saved!', style: GoogleFonts.outfit()),
backgroundColor: AppColors.sageGreen,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
_resetForm();
}
}
void _resetForm() {
setState(() {
_isPeriodDay = false;
_flowIntensity = null;
_mood = null;
_energyLevel = 3;
_crampIntensity = 0;
_hasHeadache = false;
_hasBloating = false;
_hasBreastTenderness = false;
_hasFatigue = false;
_hasAcne = false;
_notesController.clear();
});
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Text(
'How are you feeling?',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
Text(
_formatDate(DateTime.now()),
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
const SizedBox(height: 24),
// Period Toggle
_buildSectionCard(
title: 'Period',
child: Row(
children: [
Expanded(
child: Text(
'Is today a period day?',
style: GoogleFonts.outfit(
fontSize: 16,
color: AppColors.charcoal,
),
),
),
Switch(
value: _isPeriodDay,
onChanged: (value) => setState(() => _isPeriodDay = value),
activeColor: AppColors.menstrualPhase,
),
],
),
),
// Flow Intensity (only if period day)
if (_isPeriodDay) ...[
const SizedBox(height: 16),
_buildSectionCard(
title: 'Flow Intensity',
child: Row(
children: FlowIntensity.values.map((flow) {
final isSelected = _flowIntensity == flow;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _flowIntensity = flow),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isSelected
? AppColors.menstrualPhase.withOpacity(0.2)
: AppColors.lightGray.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
border: isSelected
? Border.all(color: AppColors.menstrualPhase)
: null,
),
child: Column(
children: [
Icon(
Icons.water_drop,
color: isSelected
? AppColors.menstrualPhase
: AppColors.warmGray,
size: 20,
),
const SizedBox(height: 4),
Text(
flow.label,
style: GoogleFonts.outfit(
fontSize: 11,
color: isSelected
? AppColors.menstrualPhase
: AppColors.warmGray,
),
),
],
),
),
),
);
}).toList(),
),
),
],
const SizedBox(height: 16),
// Mood
_buildSectionCard(
title: 'Mood',
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: MoodLevel.values.map((mood) {
final isSelected = _mood == mood;
return GestureDetector(
onTap: () => setState(() => _mood = mood),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected
? AppColors.softGold.withOpacity(0.2)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: isSelected
? Border.all(color: AppColors.softGold)
: null,
),
child: Column(
children: [
Text(
mood.emoji,
style: TextStyle(
fontSize: isSelected ? 32 : 28,
),
),
const SizedBox(height: 4),
Text(
mood.label,
style: GoogleFonts.outfit(
fontSize: 10,
color: isSelected
? AppColors.softGold
: AppColors.warmGray,
),
),
],
),
),
);
}).toList(),
),
),
const SizedBox(height: 16),
// Energy Level
_buildSectionCard(
title: 'Energy Level',
child: Column(
children: [
Row(
children: [
const Icon(Icons.battery_1_bar, color: AppColors.warmGray),
Expanded(
child: Slider(
value: _energyLevel.toDouble(),
min: 1,
max: 5,
divisions: 4,
onChanged: (value) {
setState(() => _energyLevel = value.round());
},
),
),
const Icon(Icons.battery_full, color: AppColors.sageGreen),
],
),
Text(
_getEnergyLabel(_energyLevel),
style: GoogleFonts.outfit(
fontSize: 13,
color: AppColors.warmGray,
),
),
],
),
),
const SizedBox(height: 16),
// Symptoms
_buildSectionCard(
title: 'Symptoms',
child: Column(
children: [
// Cramps Slider
Row(
children: [
SizedBox(
width: 80,
child: Text(
'Cramps',
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.charcoal,
),
),
),
Expanded(
child: Slider(
value: _crampIntensity.toDouble(),
min: 0,
max: 5,
divisions: 5,
activeColor: AppColors.rose,
onChanged: (value) {
setState(() => _crampIntensity = value.round());
},
),
),
SizedBox(
width: 40,
child: Text(
_crampIntensity == 0 ? 'None' : '$_crampIntensity/5',
style: GoogleFonts.outfit(
fontSize: 12,
color: AppColors.warmGray,
),
),
),
],
),
const SizedBox(height: 12),
// Symptom Toggles
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildSymptomChip('Headache', _hasHeadache, (v) => setState(() => _hasHeadache = v)),
_buildSymptomChip('Bloating', _hasBloating, (v) => setState(() => _hasBloating = v)),
_buildSymptomChip('Breast Tenderness', _hasBreastTenderness, (v) => setState(() => _hasBreastTenderness = v)),
_buildSymptomChip('Fatigue', _hasFatigue, (v) => setState(() => _hasFatigue = v)),
_buildSymptomChip('Acne', _hasAcne, (v) => setState(() => _hasAcne = v)),
],
),
],
),
),
const SizedBox(height: 16),
// Notes
_buildSectionCard(
title: 'Notes',
child: TextField(
controller: _notesController,
maxLines: 3,
decoration: InputDecoration(
hintText: 'Add any notes about how you\'re feeling...',
hintStyle: GoogleFonts.outfit(
color: AppColors.lightGray,
fontSize: 14,
),
border: InputBorder.none,
),
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.charcoal,
),
),
),
const SizedBox(height: 24),
// Save Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _saveEntry,
child: const Text('Save Entry'),
),
),
const SizedBox(height: 40),
],
),
),
);
}
Widget _buildSectionCard({required String title, required Widget child}) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
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: [
Text(
title,
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 12),
child,
],
),
);
}
Widget _buildSymptomChip(String label, bool isSelected, ValueChanged<bool> onChanged) {
return GestureDetector(
onTap: () => onChanged(!isSelected),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? AppColors.lavender.withOpacity(0.3) : AppColors.lightGray.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: isSelected ? Border.all(color: AppColors.lavender) : null,
),
child: Text(
label,
style: GoogleFonts.outfit(
fontSize: 13,
color: isSelected ? AppColors.ovulationPhase : AppColors.warmGray,
fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400,
),
),
),
);
}
String _formatDate(DateTime date) {
const months = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
return '${days[date.weekday - 1]}, ${months[date.month - 1]} ${date.day}';
}
String _getEnergyLabel(int level) {
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,642 @@
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) {
// Different background color for husband flow
final bgColor = _role == UserRole.husband ? AppColors.warmCream : AppColors.cream;
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: _role == UserRole.husband ? 2 : 5,
effect: WormEffect(
dotHeight: 8,
dotWidth: 8,
spacing: 12,
activeDotColor: _role == UserRole.husband ? AppColors.navyBlue : AppColors.sageGreen,
dotColor: AppColors.lightGray.withOpacity(0.3),
),
),
),
// 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() {
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: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
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,
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 isSelected = _role == role;
// Dynamic colors based on role selection
final activeColor = role == UserRole.wife ? AppColors.sageGreen : AppColors.navyBlue;
final activeBg = role == UserRole.wife ? AppColors.sageGreen.withOpacity(0.1) : AppColors.navyBlue.withOpacity(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 : Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? activeColor : AppColors.lightGray.withOpacity(0.5),
width: isSelected ? 2 : 1,
),
boxShadow: isSelected ? [
BoxShadow(
color: activeColor.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
)
] : [],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected ? activeColor : AppColors.lightGray.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: isSelected ? Colors.white : AppColors.warmGray,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
],
),
),
if (isSelected)
Icon(Icons.check_circle, color: activeColor),
],
),
),
);
}
Widget _buildNamePage() {
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: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: isHusband ? AppColors.navyBlue : AppColors.charcoal,
),
),
const SizedBox(height: 8),
Text(
'We\'ll use this to personalize the app.',
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
const SizedBox(height: 32),
TextField(
onChanged: (value) => setState(() => _name = value),
decoration: InputDecoration(
hintText: 'Enter your name',
prefixIcon: Icon(
Icons.person_outline,
color: AppColors.warmGray,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: activeColor),
),
),
style: GoogleFonts.outfit(fontSize: 16),
textCapitalization: TextCapitalization.words,
),
const Spacer(),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _previousPage,
style: OutlinedButton.styleFrom(
foregroundColor: isHusband ? AppColors.navyBlue : AppColors.sageGreen,
side: BorderSide(color: isHusband ? AppColors.navyBlue : AppColors.sageGreen),
),
child: const Text('Back'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: (_name.isNotEmpty && !_isNavigating) ? _nextPage : null,
style: ElevatedButton.styleFrom(
backgroundColor: activeColor,
),
child: Text(isHusband ? 'Finish Setup' : 'Continue'),
),
),
],
),
],
),
);
}
Widget _buildRelationshipPage() {
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
Text(
'Tell us about yourself',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
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: OutlinedButton(
onPressed: _previousPage,
style: OutlinedButton.styleFrom(foregroundColor: AppColors.sageGreen, side: BorderSide(color: AppColors.sageGreen)),
child: const Text('Back'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: (_relationshipStatus != null && !_isNavigating) ? _nextPage : null,
child: const Text('Continue'),
),
),
],
),
],
),
);
}
Widget _buildRelationshipOption(RelationshipStatus status, String title, String subtitle, IconData icon) {
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(0.1) : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? AppColors.sageGreen : AppColors.lightGray.withOpacity(0.5),
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
Icon(icon, color: isSelected ? AppColors.sageGreen : AppColors.warmGray),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.charcoal)),
Text(subtitle, style: GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray)),
],
),
),
if (isSelected) Icon(Icons.check_circle, color: AppColors.sageGreen),
],
),
),
);
}
Widget _buildFertilityGoalPage() {
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
Text('What\'s your goal?', style: GoogleFonts.outfit(fontSize: 28, fontWeight: FontWeight.w600, color: AppColors.charcoal)),
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: OutlinedButton(
onPressed: _previousPage,
style: OutlinedButton.styleFrom(foregroundColor: AppColors.sageGreen, side: BorderSide(color: AppColors.sageGreen)),
child: const Text('Back'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: (_fertilityGoal != null && !_isNavigating) ? _nextPage : null,
child: const Text('Continue'),
),
),
],
),
],
),
);
}
Widget _buildGoalOption(FertilityGoal goal, String title, String subtitle, IconData icon) {
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(0.1) : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? AppColors.sageGreen : AppColors.lightGray.withOpacity(0.5),
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
Icon(icon, color: isSelected ? AppColors.sageGreen : AppColors.warmGray),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.charcoal)),
Text(subtitle, style: GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray)),
],
),
),
if (isSelected) Icon(Icons.check_circle, color: AppColors.sageGreen),
],
),
),
);
}
Widget _buildCyclePage() {
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
Text('About your cycle', style: GoogleFonts.outfit(fontSize: 28, fontWeight: FontWeight.w600, color: AppColors.charcoal)),
const SizedBox(height: 32),
Text('Average cycle length', style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w500, color: AppColors.charcoal)),
Row(
children: [
Expanded(
child: Slider(
value: _averageCycleLength.toDouble(),
min: 21,
max: 40,
divisions: 19,
onChanged: (value) => setState(() => _averageCycleLength = value.round()),
),
),
Text('$_averageCycleLength days', style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.sageGreen)),
],
),
// Irregular Cycle Checkbox
CheckboxListTile(
title: Text('My cycles are irregular', style: GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal)),
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: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w500, color: AppColors.charcoal)),
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.of(context).copyWith(
colorScheme: const ColorScheme.light(primary: AppColors.sageGreen, onPrimary: Colors.white, surface: Colors.white, onSurface: AppColors.charcoal),
),
child: child!,
);
},
);
if (date != null) setState(() => _lastPeriodStart = date);
},
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.lightGray.withOpacity(0.5))),
child: Row(
children: [
Icon(Icons.calendar_today, color: AppColors.warmGray),
const SizedBox(width: 12),
Text(_lastPeriodStart != null ? "${_lastPeriodStart!.month}/${_lastPeriodStart!.day}/${_lastPeriodStart!.year}" : "Select Date", style: GoogleFonts.outfit(fontSize: 16, color: AppColors.charcoal)),
],
),
),
),
const Spacer(),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _previousPage,
style: OutlinedButton.styleFrom(foregroundColor: AppColors.sageGreen, side: BorderSide(color: AppColors.sageGreen)),
child: const Text('Back'),
),
),
const SizedBox(width: 16),
Expanded(
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: 2500), () {
_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,65 @@
import '../models/user_profile.dart';
import '../models/cycle_entry.dart';
class CycleService {
/// Calculates the current cycle information based on user profile
static Map<String, dynamic> calculateCycleInfo(UserProfile? user) {
if (user?.lastPeriodStartDate == null) {
return {
'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 {
'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.';
}
}
}

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

@@ -0,0 +1,307 @@
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: CardTheme(
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,
colorScheme: const ColorScheme.dark(
primary: AppColors.sageGreen,
secondary: AppColors.rose,
tertiary: AppColors.lavender,
surface: Color(0xFF1E1E1E),
error: AppColors.error,
),
scaffoldBackgroundColor: const Color(0xFF121212),
textTheme: TextTheme(
displayLarge: GoogleFonts.outfit(
fontSize: 32,
fontWeight: FontWeight.w600,
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,
),
),
cardTheme: CardTheme(
color: const Color(0xFF1E1E1E),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
);
}
}
/// Scripture text style
TextStyle scriptureStyle(BuildContext context, {double? fontSize}) {
return GoogleFonts.lora(
fontSize: fontSize ?? 16,
fontStyle: FontStyle.italic,
color: AppColors.charcoal,
height: 1.6,
);
}
/// Scripture reference style
TextStyle scriptureRefStyle(BuildContext context) {
return GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
);
}

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

@@ -0,0 +1,195 @@
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 StatelessWidget {
final int dayOfCycle;
final int totalDays;
final CyclePhase phase;
const CycleRing({
super.key,
required this.dayOfCycle,
required this.totalDays,
required this.phase,
});
@override
Widget build(BuildContext context) {
final progress = dayOfCycle / totalDays;
final daysUntilNextPeriod = totalDays - dayOfCycle;
return Container(
width: 220,
height: 220,
child: Stack(
alignment: Alignment.center,
children: [
// Background ring
CustomPaint(
size: const Size(220, 220),
painter: _CycleRingPainter(
progress: progress,
phase: phase,
),
),
// Center content
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Day $dayOfCycle',
style: GoogleFonts.outfit(
fontSize: 32,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
phase.emoji,
style: const TextStyle(fontSize: 14),
),
const SizedBox(width: 6),
Text(
phase.label,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: _getPhaseColor(phase),
),
),
],
),
),
const SizedBox(height: 8),
Text(
daysUntilNextPeriod > 0
? '$daysUntilNextPeriod days until period'
: 'Period expected',
style: GoogleFonts.outfit(
fontSize: 12,
color: AppColors.warmGray,
),
),
],
),
],
),
);
}
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;
_CycleRingPainter({required this.progress, required this.phase});
@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 = AppColors.lightGray.withOpacity(0.2)
..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 = 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,95 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../theme/app_theme.dart';
import '../screens/log/log_screen.dart';
class QuickLogButtons extends StatelessWidget {
const QuickLogButtons({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
_buildQuickButton(
icon: Icons.water_drop_outlined,
label: 'Period',
color: AppColors.menstrualPhase,
onTap: () => _navigateToLog(context),
),
const SizedBox(width: 12),
_buildQuickButton(
icon: Icons.emoji_emotions_outlined,
label: 'Mood',
color: AppColors.softGold,
onTap: () => _navigateToLog(context),
),
const SizedBox(width: 12),
_buildQuickButton(
icon: Icons.flash_on_outlined,
label: 'Energy',
color: AppColors.follicularPhase,
onTap: () => _navigateToLog(context),
),
const SizedBox(width: 12),
_buildQuickButton(
icon: Icons.healing_outlined,
label: 'Symptoms',
color: AppColors.lavender,
onTap: () => _navigateToLog(context),
),
],
);
}
void _navigateToLog(BuildContext context) {
// Navigate to the Log tab (index 2) of HomeScreen if possible,
// but since we are inside a tab, we can't easily switch the parent tab index without context. Using a provider or callback would be best.
// For now, let's push the LogScreen as a new route for "Quick Log" feel.
// Ideally we would switch the BottomNavBar index.
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(0),
child: SizedBox.shrink()
),
body: LogScreen()
)),
);
}
Widget _buildQuickButton({
required IconData icon,
required String label,
required Color color,
required VoidCallback onTap,
}) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 24),
const SizedBox(height: 6),
Text(
label,
style: GoogleFonts.outfit(
fontSize: 11,
fontWeight: FontWeight.w500,
color: color,
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,134 @@
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 CyclePhase phase;
const ScriptureCard({
super.key,
required this.verse,
required this.reference,
required this.phase,
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: _getGradientColors(phase),
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: _getPhaseColor(phase).withOpacity(0.2),
blurRadius: 15,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Scripture icon
Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.menu_book_outlined,
size: 18,
color: AppColors.charcoal.withOpacity(0.8),
),
),
const SizedBox(width: 8),
Text(
'Today\'s Verse',
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.charcoal.withOpacity(0.7),
letterSpacing: 0.5,
),
),
],
),
const SizedBox(height: 16),
// Verse
Text(
'"$verse"',
style: GoogleFonts.lora(
fontSize: 16,
fontStyle: FontStyle.italic,
color: AppColors.charcoal,
height: 1.6,
),
),
const SizedBox(height: 12),
// Reference
Text(
'$reference',
style: GoogleFonts.outfit(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
],
),
);
}
List<Color> _getGradientColors(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return [
AppColors.blushPink.withOpacity(0.6),
AppColors.cream,
];
case CyclePhase.follicular:
return [
AppColors.sageGreen.withOpacity(0.3),
AppColors.cream,
];
case CyclePhase.ovulation:
return [
AppColors.lavender.withOpacity(0.5),
AppColors.cream,
];
case CyclePhase.luteal:
return [
AppColors.lutealPhase.withOpacity(0.3),
AppColors.cream,
];
}
}
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;
}
}
}

95
lib/widgets/tip_card.dart Normal file
View File

@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../theme/app_theme.dart';
import '../models/cycle_entry.dart';
class TipCard extends StatelessWidget {
final CyclePhase phase;
final bool isMarried;
const TipCard({
super.key,
required this.phase,
required this.isMarried,
});
@override
Widget build(BuildContext context) {
final tip = _getTipForPhase(phase, isMarried);
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
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: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.sageGreen.withOpacity(0.15),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.lightbulb_outline,
color: AppColors.sageGreen,
size: 22,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Today\'s Tip',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 4),
Text(
tip,
style: GoogleFonts.outfit(
fontSize: 13,
color: AppColors.warmGray,
height: 1.4,
),
),
],
),
),
],
),
);
}
String _getTipForPhase(CyclePhase phase, bool isMarried) {
switch (phase) {
case CyclePhase.menstrual:
return 'This is a time for rest. Honor your body with extra sleep, warm drinks, and gentle movement. God designed your body with wisdom.';
case CyclePhase.follicular:
return 'Your energy is rising! This is a great time to start new projects, exercise more intensely, and spend time in community.';
case CyclePhase.ovulation:
if (isMarried) {
return 'This is your most fertile window. You may feel more social and energetic. Prioritize connection with your spouse.';
}
return 'You may feel more social and confident during this phase. It\'s a great time for important conversations and presentations.';
case CyclePhase.luteal:
return 'As you enter the luteal phase, focus on nourishing foods, adequate sleep, and stress management. Be gentle with yourself.';
}
}
}

1
linux/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
flutter/ephemeral

145
linux/CMakeLists.txt Normal file
View File

@@ -0,0 +1,145 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "christian_period_tracker")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.faithapps.christian_period_tracker")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Define the application target. To change its name, change BINARY_NAME above,
# not the value here, or `flutter run` will no longer work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View File

@@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View File

@@ -0,0 +1,11 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void fl_register_plugins(FlPluginRegistry* registry) {
}

View File

@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@@ -0,0 +1,23 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

6
linux/main.cc Normal file
View File

@@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

124
linux/my_application.cc Normal file
View File

@@ -0,0 +1,124 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "christian_period_tracker");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "christian_period_tracker");
}
gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
//MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
//MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE,
nullptr));
}

18
linux/my_application.h Normal file
View File

@@ -0,0 +1,18 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

7
macos/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import flutter_local_notifications
import path_provider_foundation
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}

View File

@@ -0,0 +1,705 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXAggregateTarget section */
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
isa = PBXAggregateTarget;
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
buildPhases = (
33CC111E2044C6BF0003C045 /* ShellScript */,
);
dependencies = (
);
name = "Flutter Assemble";
productName = FLX;
};
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC10EC2044A3C60003C045;
remoteInfo = Runner;
};
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
remoteInfo = FLX;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
33CC110E2044A8840003C045 /* Bundle Framework */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Bundle Framework";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* christian_period_tracker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "christian_period_tracker.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
331C80D2294CF70F00263BE5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EA2044A3C60003C045 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C80D6294CF71000263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C80D7294CF71000263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
33BA886A226E78AF003329D5 /* Configs */ = {
isa = PBXGroup;
children = (
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
);
path = Configs;
sourceTree = "<group>";
};
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
);
sourceTree = "<group>";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* christian_period_tracker.app */,
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
name = Resources;
path = ..;
sourceTree = "<group>";
};
33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup;
children = (
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
);
path = Flutter;
sourceTree = "<group>";
};
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
33BA886A226E78AF003329D5 /* Configs */,
);
path = Runner;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C80D4294CF70F00263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C80DA294CF71000263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
33CC10EC2044A3C60003C045 /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
);
buildRules = (
);
dependencies = (
33CC11202044C79F0003C045 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* christian_period_tracker.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
33CC10E52044A3C60003C045 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C80D4294CF70F00263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 33CC10EC2044A3C60003C045;
};
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
};
};
};
33CC111A2044C6BA0003C045 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Manual;
};
};
};
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 33CC10E42044A3C60003C045;
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
33CC10EC2044A3C60003C045 /* Runner */,
331C80D4294CF70F00263BE5 /* RunnerTests */,
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C80D3294CF70F00263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EB2044A3C60003C045 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
};
33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
Flutter/ephemeral/FlutterInputs.xcfilelist,
);
inputPaths = (
Flutter/ephemeral/tripwire,
);
outputFileListPaths = (
Flutter/ephemeral/FlutterOutputs.xcfilelist,
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C80D1294CF70F00263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10E92044A3C60003C045 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC10EC2044A3C60003C045 /* Runner */;
targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
};
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
33CC10F52044A3C60003C045 /* Base */,
);
name = MainMenu.xib;
path = Runner;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
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)/christian_period_tracker.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/christian_period_tracker";
};
name = Debug;
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
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)/christian_period_tracker.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/christian_period_tracker";
};
name = Release;
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
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)/christian_period_tracker.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/christian_period_tracker";
};
name = Profile;
};
338D0CE9231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
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_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
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_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Profile;
};
338D0CEA231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Profile;
};
338D0CEB231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Profile;
};
33CC10F92044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
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_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
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_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
33CC10FA2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
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_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
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_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
33CC10FC2044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
33CC10FD2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
33CC111C2044C6BA0003C045 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
33CC111D2044C6BA0003C045 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C80DB294CF71000263BE5 /* Debug */,
331C80DC294CF71000263BE5 /* Release */,
331C80DD294CF71000263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10F92044A3C60003C045 /* Debug */,
33CC10FA2044A3C60003C045 /* Release */,
338D0CE9231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10FC2044A3C60003C045 /* Debug */,
33CC10FD2044A3C60003C045 /* Release */,
338D0CEA231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC111C2044C6BA0003C045 /* Debug */,
33CC111D2044C6BA0003C045 /* Release */,
338D0CEB231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}

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,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 = "33CC10EC2044A3C60003C045"
BuildableName = "christian_period_tracker.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 = "33CC10EC2044A3C60003C045"
BuildableName = "christian_period_tracker.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
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 = "33CC10EC2044A3C60003C045"
BuildableName = "christian_period_tracker.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 = "33CC10EC2044A3C60003C045"
BuildableName = "christian_period_tracker.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>

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