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