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.
232 lines
8.2 KiB
Dart
232 lines
8.2 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:xml/xml.dart';
|
|
|
|
// Copy of book abbreviations from BibleXmlParser
|
|
const Map<String, String> _bookAbbreviations = {
|
|
'genesis': 'Gen', 'exodus': 'Exod', 'leviticus': 'Lev', 'numbers': 'Num',
|
|
'deuteronomy': 'Deut', 'joshua': 'Josh', 'judges': 'Judg', 'ruth': 'Ruth',
|
|
'1 samuel': '1Sam', '2 samuel': '2Sam', '1 kings': '1Kgs', '2 kings': '2Kgs',
|
|
'1 chronicles': '1Chr', '2 chronicles': '2Chr', 'ezra': 'Ezra', 'nehemiah': 'Neh',
|
|
'esther': 'Esth', 'job': 'Job', 'psalm': 'Ps', 'proverbs': 'Prov',
|
|
'ecclesiastes': 'Eccl', 'song of solomon': 'Song', 'isaiah': 'Isa', 'jeremiah': 'Jer',
|
|
'lamentations': 'Lam', 'ezekiel': 'Ezek', 'daniel': 'Dan', 'hosea': 'Hos',
|
|
'joel': 'Joel', 'amos': 'Amos', 'obadiah': 'Obad', 'jonah': 'Jonah',
|
|
'micah': 'Mic', 'nahum': 'Nah', 'habakkuk': 'Hab', 'zephaniah': 'Zeph',
|
|
'haggai': 'Hag', 'zechariah': 'Zech', 'malachi': 'Mal',
|
|
'matthew': 'Matt', 'mark': 'Mark', 'luke': 'Luke', 'john': 'John',
|
|
'acts': 'Acts', 'romans': 'Rom', '1 corinthians': '1Cor', '2 corinthians': '2Cor',
|
|
'galatians': 'Gal', 'ephesians': 'Eph', 'philippians': 'Phil', 'colossians': 'Col',
|
|
'1 thessalonians': '1Thess', '2 thessalonians': '2Thess', '1 timothy': '1Tim',
|
|
'2 timothy': '2Tim', 'titus': 'Titus', 'philemon': 'Phlm', 'hebrews': 'Heb',
|
|
'james': 'Jas', '1 peter': '1Pet', '2 peter': '2Pet', '1 john': '1John',
|
|
'2 john': '2John', '3 john': '3John', 'jude': 'Jude', 'revelation': 'Rev',
|
|
};
|
|
|
|
// Map of translations to filenames
|
|
final Map<String, String> _translationFiles = {
|
|
'esv': 'assets/bible_xml/ESV.xml',
|
|
'niv': 'assets/bible_xml/NIV.xml',
|
|
'nkjv': 'assets/bible_xml/NKJV.xml',
|
|
'nlt': 'assets/bible_xml/NLT.xml',
|
|
'nasb': 'assets/bible_xml/NASB.xml',
|
|
'kjv': 'assets/bible_xml/KJV.xml',
|
|
'msg': 'assets/bible_xml/MSG.xml',
|
|
};
|
|
|
|
void main() async {
|
|
print('Starting asset optimization...');
|
|
|
|
// 1. Load the base JSON
|
|
final File jsonFile = File('assets/scriptures.json');
|
|
if (!jsonFile.existsSync()) {
|
|
print('Error: assets/scriptures.json not found.');
|
|
return;
|
|
}
|
|
final Map<String, dynamic> data = json.decode(jsonFile.readAsStringSync());
|
|
|
|
// 2. Load and Parse all XMLs
|
|
final Map<String, XmlDocument> xmlDocs = {};
|
|
for (var entry in _translationFiles.entries) {
|
|
final key = entry.key;
|
|
final path = entry.value;
|
|
final file = File(path);
|
|
if (file.existsSync()) {
|
|
print('Parsing $key from $path...');
|
|
try {
|
|
xmlDocs[key] = XmlDocument.parse(file.readAsStringSync());
|
|
} catch (e) {
|
|
print('Error parsing $path: $e');
|
|
}
|
|
} else {
|
|
print('Warning: $path not found.');
|
|
}
|
|
}
|
|
|
|
// 3. Process the JSON structure
|
|
for (var phase in ['menstrual', 'follicular', 'ovulation', 'luteal']) {
|
|
if (data[phase] != null) {
|
|
await _processList(data[phase] as List, xmlDocs);
|
|
}
|
|
}
|
|
|
|
if (data['husband'] != null) {
|
|
await _processList(data['husband'] as List, xmlDocs);
|
|
}
|
|
|
|
if (data['womanhood'] != null) {
|
|
await _processList(data['womanhood'] as List, xmlDocs);
|
|
}
|
|
|
|
if (data['contextual'] != null) {
|
|
final contextualMap = data['contextual'] as Map<String, dynamic>;
|
|
for (var key in contextualMap.keys) {
|
|
await _processList(contextualMap[key] as List, xmlDocs);
|
|
}
|
|
}
|
|
|
|
// 4. Write the optimized JSON
|
|
final outputFile = File('assets/scriptures_optimized.json');
|
|
outputFile.writeAsStringSync(json.encode(data)); // Minified
|
|
// outputFile.writeAsStringSync(const JsonEncoder.withIndent(' ').convert(data)); // Pretty print
|
|
|
|
print('Optimization complete. Wrote to assets/scriptures_optimized.json');
|
|
}
|
|
|
|
Future<void> _processList(List list, Map<String, XmlDocument> xmlDocs) async {
|
|
for (var item in list) {
|
|
final String reference = item['reference'];
|
|
Map<String, dynamic> verses = item['verses'] ?? {};
|
|
|
|
// Parse reference
|
|
final parts = _parseReference(reference);
|
|
if (parts == null) {
|
|
print('Skipping invalid reference: $reference');
|
|
continue;
|
|
}
|
|
|
|
// Look up for each translation
|
|
for (var entry in _translationFiles.entries) {
|
|
final transKey = entry.key; // esv, niv, etc.
|
|
|
|
// If already has text, skip (or overwrite? Let's overwrite to ensure consistency,
|
|
// but the original JSON had manual entries. The user wants to use the XMLs.
|
|
// Let's only fill if missing or if we want to enforce XML source.)
|
|
// Strategy: Fill if we have XML data.
|
|
|
|
if (xmlDocs.containsKey(transKey)) {
|
|
final text = _getVerseFromXml(
|
|
xmlDocs[transKey]!,
|
|
parts['book']!,
|
|
int.parse(parts['chapter']!),
|
|
int.parse(parts['verse']!)
|
|
);
|
|
|
|
if (text != null) {
|
|
verses[transKey] = text;
|
|
} else {
|
|
print('Warning: Could not find $reference in $transKey');
|
|
}
|
|
}
|
|
}
|
|
item['verses'] = verses;
|
|
}
|
|
}
|
|
|
|
Map<String, String>? _parseReference(String reference) {
|
|
final parts = reference.split(' ');
|
|
if (parts.length < 2) return null;
|
|
|
|
String book = parts.sublist(0, parts.length - 1).join(' ').toLowerCase();
|
|
String chapterVerse = parts.last;
|
|
final cvParts = chapterVerse.split(':');
|
|
if (cvParts.length < 2) return null; // Only handles simple Chapter:Verse for now
|
|
|
|
return {
|
|
'book': book,
|
|
'chapter': cvParts[0],
|
|
// Handle cases like "1-2" by just taking the first one?
|
|
// The current parser Logic only takes one verse number.
|
|
// If the reference is "Psalm 23:1-2", the XML parser expects a single integer.
|
|
// We should probably just parse the first verse or handle ranges?
|
|
// For this fix, let's just parse the start verse to prevent crashing on int.parse
|
|
'verse': cvParts[1].split('-')[0],
|
|
};
|
|
}
|
|
|
|
String? _getVerseFromXml(XmlDocument document, String bookName, int chapterNum, int verseNum) {
|
|
// Standardize book name for lookup
|
|
String lookupBookName = _bookAbbreviations[bookName.toLowerCase()] ?? bookName;
|
|
|
|
// Use root element to avoid full document search
|
|
XmlElement root = document.rootElement;
|
|
|
|
// -- Find Book --
|
|
// Try Schema 1: Direct child of root
|
|
var bookElement = root.findElements('BIBLEBOOK').firstWhere(
|
|
(element) {
|
|
final nameAttr = element.getAttribute('bname');
|
|
return nameAttr?.toLowerCase() == lookupBookName.toLowerCase() ||
|
|
nameAttr?.toLowerCase() == bookName.toLowerCase();
|
|
},
|
|
orElse: () => XmlElement(XmlName('notfound')),
|
|
);
|
|
|
|
// Try Schema 2 if not found
|
|
if (bookElement.name.local == 'notfound') {
|
|
bookElement = root.findElements('b').firstWhere(
|
|
(element) {
|
|
final nameAttr = element.getAttribute('n');
|
|
return nameAttr?.toLowerCase() == lookupBookName.toLowerCase() ||
|
|
nameAttr?.toLowerCase() == bookName.toLowerCase();
|
|
},
|
|
orElse: () => XmlElement(XmlName('notfound')),
|
|
);
|
|
}
|
|
|
|
if (bookElement.name.local == 'notfound') {
|
|
return null;
|
|
}
|
|
|
|
// -- Find Chapter --
|
|
// Try Schema 1: Direct child of book
|
|
var chapterElement = bookElement.findElements('CHAPTER').firstWhere(
|
|
(element) => element.getAttribute('cnumber') == chapterNum.toString(),
|
|
orElse: () => XmlElement(XmlName('notfound')),
|
|
);
|
|
|
|
// Try Schema 2 if not found
|
|
if (chapterElement.name.local == 'notfound') {
|
|
chapterElement = bookElement.findElements('c').firstWhere(
|
|
(element) => element.getAttribute('n') == chapterNum.toString(),
|
|
orElse: () => XmlElement(XmlName('notfound')),
|
|
);
|
|
}
|
|
|
|
if (chapterElement.name.local == 'notfound') {
|
|
return null;
|
|
}
|
|
|
|
// -- Find Verse --
|
|
// Try Schema 1: Direct child of chapter
|
|
var verseElement = chapterElement.findElements('VERS').firstWhere(
|
|
(element) => element.getAttribute('vnumber') == verseNum.toString(),
|
|
orElse: () => XmlElement(XmlName('notfound')),
|
|
);
|
|
|
|
// Try Schema 2 if not found
|
|
if (verseElement.name.local == 'notfound') {
|
|
verseElement = chapterElement.findElements('v').firstWhere(
|
|
(element) => element.getAttribute('n') == verseNum.toString(),
|
|
orElse: () => XmlElement(XmlName('notfound')),
|
|
);
|
|
}
|
|
|
|
if (verseElement.name.local == 'notfound') {
|
|
return null;
|
|
}
|
|
|
|
// Extract the text content of the verse
|
|
return verseElement.innerText.trim();
|
|
}
|