Initial commit: Fixes for linting and compilation
This commit is contained in:
49
lib/main.dart
Normal file
49
lib/main.dart
Normal 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
335
lib/models/cycle_entry.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
310
lib/models/cycle_entry.g.dart
Normal file
310
lib/models/cycle_entry.g.dart
Normal 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
235
lib/models/scripture.dart
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
163
lib/models/user_profile.dart
Normal file
163
lib/models/user_profile.dart
Normal 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,
|
||||
}
|
||||
210
lib/models/user_profile.g.dart
Normal file
210
lib/models/user_profile.g.dart
Normal 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;
|
||||
}
|
||||
75
lib/providers/user_provider.dart
Normal file
75
lib/providers/user_provider.dart
Normal 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);
|
||||
});
|
||||
412
lib/screens/calendar/calendar_screen.dart
Normal file
412
lib/screens/calendar/calendar_screen.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
372
lib/screens/devotional/devotional_screen.dart
Normal file
372
lib/screens/devotional/devotional_screen.dart
Normal 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."';
|
||||
}
|
||||
}
|
||||
}
|
||||
406
lib/screens/home/home_screen.dart
Normal file
406
lib/screens/home/home_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
831
lib/screens/husband/husband_home_screen.dart
Normal file
831
lib/screens/husband/husband_home_screen.dart
Normal 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: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
450
lib/screens/log/log_screen.dart
Normal file
450
lib/screens/log/log_screen.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
642
lib/screens/onboarding/onboarding_screen.dart
Normal file
642
lib/screens/onboarding/onboarding_screen.dart
Normal 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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
183
lib/screens/splash_screen.dart
Normal file
183
lib/screens/splash_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
65
lib/services/cycle_service.dart
Normal file
65
lib/services/cycle_service.dart
Normal 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
307
lib/theme/app_theme.dart
Normal 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
195
lib/widgets/cycle_ring.dart
Normal 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;
|
||||
}
|
||||
95
lib/widgets/quick_log_buttons.dart
Normal file
95
lib/widgets/quick_log_buttons.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
134
lib/widgets/scripture_card.dart
Normal file
134
lib/widgets/scripture_card.dart
Normal 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
95
lib/widgets/tip_card.dart
Normal 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.';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user