Resolve all lints and deprecation warnings
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../models/user_profile.dart';
|
||||
import '../models/scripture.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../providers/user_provider.dart';
|
||||
|
||||
class BibleUtils {
|
||||
static Future<void> showTranslationPicker(BuildContext context, WidgetRef ref) async {
|
||||
static Future<void> showTranslationPicker(
|
||||
BuildContext context, WidgetRef ref) async {
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user == null) return;
|
||||
|
||||
@@ -28,21 +28,21 @@ class BibleUtils {
|
||||
),
|
||||
),
|
||||
...BibleTranslation.values.map((t) => ListTile(
|
||||
title: Text(t.label),
|
||||
trailing: user.bibleTranslation == t
|
||||
? const Icon(Icons.check, color: AppColors.sageGreen)
|
||||
: null,
|
||||
onTap: () => Navigator.pop(context, t),
|
||||
)),
|
||||
title: Text(t.label),
|
||||
trailing: user.bibleTranslation == t
|
||||
? const Icon(Icons.check, color: AppColors.sageGreen)
|
||||
: null,
|
||||
onTap: () => Navigator.pop(context, t),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (selected != null) {
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
user.copyWith(bibleTranslation: selected)
|
||||
);
|
||||
await ref
|
||||
.read(userProfileProvider.notifier)
|
||||
.updateProfile(user.copyWith(bibleTranslation: selected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
import '../models/scripture.dart'; // Assuming Scripture model might need BibleTranslation
|
||||
|
||||
// Helper for background XML parsing
|
||||
XmlDocument parseXmlString(String xml) {
|
||||
@@ -14,18 +13,24 @@ class BibleXmlParser {
|
||||
static const Map<String, String> _bookAbbreviations = {
|
||||
'genesis': 'Gen', 'exodus': 'Exod', 'leviticus': 'Lev', 'numbers': 'Num',
|
||||
'deuteronomy': 'Deut', 'joshua': 'Josh', 'judges': 'Judg', 'ruth': 'Ruth',
|
||||
'1 samuel': '1Sam', '2 samuel': '2Sam', '1 kings': '1Kgs', '2 kings': '2Kgs',
|
||||
'1 chronicles': '1Chr', '2 chronicles': '2Chr', 'ezra': 'Ezra', 'nehemiah': 'Neh',
|
||||
'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',
|
||||
'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',
|
||||
'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',
|
||||
@@ -50,7 +55,6 @@ class BibleXmlParser {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Cache for parsed XML documents to avoid reloading/reparsing
|
||||
static final Map<String, XmlDocument> _xmlCache = {};
|
||||
|
||||
@@ -59,8 +63,8 @@ class BibleXmlParser {
|
||||
if (_xmlCache.containsKey(assetPath)) {
|
||||
return _xmlCache[assetPath]!;
|
||||
}
|
||||
|
||||
print('Loading and parsing XML asset: $assetPath'); // Debug log
|
||||
|
||||
debugPrint('Loading and parsing XML asset: $assetPath'); // Debug log
|
||||
final String xmlString = await rootBundle.loadString(assetPath);
|
||||
final document = await compute(parseXmlString, xmlString);
|
||||
_xmlCache[assetPath] = document;
|
||||
@@ -71,13 +75,11 @@ class BibleXmlParser {
|
||||
/// Supports two schemas:
|
||||
/// 1. <XMLBIBLE><BIBLEBOOK bname="..."><CHAPTER cnumber="..."><VERS vnumber="...">
|
||||
/// 2. <bible><b n="..."><c n="..."><v n="...">
|
||||
/// Extracts a specific verse from a parsed XML document.
|
||||
/// Supports two schemas:
|
||||
/// 1. <XMLBIBLE><BIBLEBOOK bname="..."><CHAPTER cnumber="..."><VERS vnumber="...">
|
||||
/// 2. <bible><b n="..."><c n="..."><v n="...">
|
||||
String? getVerseFromXml(XmlDocument document, String bookName, int chapterNum, int verseNum) {
|
||||
String? getVerseFromXml(
|
||||
XmlDocument document, String bookName, int chapterNum, int verseNum) {
|
||||
// Standardize book name for lookup
|
||||
String lookupBookName = _bookAbbreviations[bookName.toLowerCase()] ?? bookName;
|
||||
String lookupBookName =
|
||||
_bookAbbreviations[bookName.toLowerCase()] ?? bookName;
|
||||
|
||||
// Use root element to avoid full document search
|
||||
XmlElement root = document.rootElement;
|
||||
@@ -88,7 +90,7 @@ class BibleXmlParser {
|
||||
(element) {
|
||||
final nameAttr = element.getAttribute('bname');
|
||||
return nameAttr?.toLowerCase() == lookupBookName.toLowerCase() ||
|
||||
nameAttr?.toLowerCase() == bookName.toLowerCase();
|
||||
nameAttr?.toLowerCase() == bookName.toLowerCase();
|
||||
},
|
||||
orElse: () => XmlElement(XmlName('notfound')),
|
||||
);
|
||||
@@ -99,54 +101,54 @@ class BibleXmlParser {
|
||||
(element) {
|
||||
final nameAttr = element.getAttribute('n');
|
||||
return nameAttr?.toLowerCase() == lookupBookName.toLowerCase() ||
|
||||
nameAttr?.toLowerCase() == bookName.toLowerCase();
|
||||
nameAttr?.toLowerCase() == bookName.toLowerCase();
|
||||
},
|
||||
orElse: () => XmlElement(XmlName('notfound')),
|
||||
);
|
||||
}
|
||||
|
||||
if (bookElement.name.local == 'notfound') {
|
||||
// print('Book "$bookName" not found in XML.'); // Commented out to reduce log spam
|
||||
// debugPrint('Book "$bookName" not found in XML.'); // Commented out to reduce log spam
|
||||
return null;
|
||||
}
|
||||
|
||||
// -- Find Chapter --
|
||||
// Try Schema 1: Direct child of book
|
||||
var chapterElement = bookElement.findElements('CHAPTER').firstWhere(
|
||||
(element) => element.getAttribute('cnumber') == chapterNum.toString(),
|
||||
orElse: () => XmlElement(XmlName('notfound')),
|
||||
);
|
||||
(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')),
|
||||
);
|
||||
(element) => element.getAttribute('n') == chapterNum.toString(),
|
||||
orElse: () => XmlElement(XmlName('notfound')),
|
||||
);
|
||||
}
|
||||
|
||||
if (chapterElement.name.local == 'notfound') {
|
||||
// print('Chapter "$chapterNum" not found for book "$bookName".');
|
||||
// debugPrint('Chapter "$chapterNum" not found for book "$bookName".');
|
||||
return null;
|
||||
}
|
||||
|
||||
// -- Find Verse --
|
||||
// Try Schema 1: Direct child of chapter
|
||||
var verseElement = chapterElement.findElements('VERS').firstWhere(
|
||||
(element) => element.getAttribute('vnumber') == verseNum.toString(),
|
||||
orElse: () => XmlElement(XmlName('notfound')),
|
||||
);
|
||||
(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')),
|
||||
);
|
||||
(element) => element.getAttribute('n') == verseNum.toString(),
|
||||
orElse: () => XmlElement(XmlName('notfound')),
|
||||
);
|
||||
}
|
||||
|
||||
if (verseElement.name.local == 'notfound') {
|
||||
// print('Verse "$verseNum" not found for Chapter "$chapterNum", book "$bookName".');
|
||||
// debugPrint('Verse "$verseNum" not found for Chapter "$chapterNum", book "$bookName".');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -158,7 +160,7 @@ class BibleXmlParser {
|
||||
Future<String?> getVerseFromAsset(String assetPath, String reference) async {
|
||||
final parsedRef = parseReference(reference);
|
||||
if (parsedRef == null) {
|
||||
print('Invalid reference format: $reference');
|
||||
debugPrint('Invalid reference format: $reference');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,9 +36,10 @@ class CycleInfo {
|
||||
class CycleService {
|
||||
/// Calculates the current cycle information based on user profile
|
||||
/// Calculates the current cycle information based on user profile and cycle entries
|
||||
static CycleInfo calculateCycleInfo(UserProfile? user, List<CycleEntry> entries) {
|
||||
static CycleInfo calculateCycleInfo(
|
||||
UserProfile? user, List<CycleEntry> entries) {
|
||||
if (user == null) {
|
||||
return CycleInfo(
|
||||
return CycleInfo(
|
||||
phase: CyclePhase.follicular,
|
||||
dayOfCycle: 1,
|
||||
daysUntilPeriod: 28,
|
||||
@@ -51,69 +52,70 @@ class CycleService {
|
||||
// Find the most recent period start from entries if available and more recent
|
||||
// We look for a sequence of period days and take the first one
|
||||
if (entries.isNotEmpty) {
|
||||
final sortedEntries = List<CycleEntry>.from(entries)..sort((a, b) => b.date.compareTo(a.date));
|
||||
|
||||
final sortedEntries = List<CycleEntry>.from(entries)
|
||||
..sort((a, b) => b.date.compareTo(a.date));
|
||||
|
||||
for (var entry in sortedEntries) {
|
||||
if (entry.isPeriodDay) {
|
||||
// Check if this is a "start" of a period (previous day was not period or no entry)
|
||||
// Simplified logic: Just take the most recent period day found and assume it's part of the current/last period
|
||||
// A better approach for "Day 1" is to find the First day of the contiguous block.
|
||||
|
||||
// However, for basic calculation, if we find a period day at Date X,
|
||||
// and today is Date Y.
|
||||
// If X is very recent, we are in the period.
|
||||
|
||||
// Correct logic: Identify the START DATE of the last period group.
|
||||
// 1. Find the latest period entry.
|
||||
// 2. Look backwards from there as long as there are consecutive period days.
|
||||
|
||||
DateTime potentialStart = entry.date;
|
||||
|
||||
// Check if we have a period day "tomorrow" relative to this entry? No, we are iterating backwards (descending).
|
||||
// So if we found a period day, we need to check if the NEXT entry (which is earlier in time) is also a period day.
|
||||
// If so, that earlier day is the better candidate for "Start".
|
||||
|
||||
// Let's iterate linearly.
|
||||
// Since we sorted DESC, `entry` is the LATEST period day.
|
||||
// We need to see if there are consecutive period days before it.
|
||||
|
||||
// But wait, the user might have logged Day 1, Day 2, Day 3.
|
||||
// `entry` will be Day 3.
|
||||
// We want Day 1.
|
||||
|
||||
// Let's try a different approach:
|
||||
// Get all period days sorted DESC.
|
||||
final periodDays = sortedEntries.where((e) => e.isPeriodDay).toList();
|
||||
|
||||
if (periodDays.isNotEmpty) {
|
||||
// Take the latest block
|
||||
DateTime latestParams = periodDays.first.date;
|
||||
|
||||
// Now find the "start" of this block
|
||||
// We iterate backwards from the *latest* date found
|
||||
|
||||
DateTime currentSearch = latestParams;
|
||||
DateTime startOfBlock = latestParams;
|
||||
|
||||
// Check if we have an entry for the day before
|
||||
bool foundPrevious = true;
|
||||
while(foundPrevious) {
|
||||
final dayBefore = currentSearch.subtract(const Duration(days: 1));
|
||||
final hasDayBefore = periodDays.any((e) => DateUtils.isSameDay(e.date, dayBefore));
|
||||
if (hasDayBefore) {
|
||||
currentSearch = dayBefore;
|
||||
startOfBlock = dayBefore;
|
||||
} else {
|
||||
foundPrevious = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If this calculated start is more recent than the user profile one, use it
|
||||
if (lastPeriodStart == null || startOfBlock.isAfter(lastPeriodStart)) {
|
||||
lastPeriodStart = startOfBlock;
|
||||
}
|
||||
}
|
||||
break; // We only care about the most recent period block
|
||||
// Check if this is a "start" of a period (previous day was not period or no entry)
|
||||
// Simplified logic: Just take the most recent period day found and assume it's part of the current/last period
|
||||
// A better approach for "Day 1" is to find the First day of the contiguous block.
|
||||
|
||||
// However, for basic calculation, if we find a period day at Date X,
|
||||
// and today is Date Y.
|
||||
// If X is very recent, we are in the period.
|
||||
|
||||
// Correct logic: Identify the START DATE of the last period group.
|
||||
// 1. Find the latest period entry.
|
||||
// 2. Look backwards from there as long as there are consecutive period days.
|
||||
|
||||
// Check if we have a period day "tomorrow" relative to this entry? No, we are iterating backwards (descending).
|
||||
// So if we found a period day, we need to check if the NEXT entry (which is earlier in time) is also a period day.
|
||||
// If so, that earlier day is the better candidate for "Start".
|
||||
|
||||
// Let's iterate linearly.
|
||||
// Since we sorted DESC, `entry` is the LATEST period day.
|
||||
// We need to see if there are consecutive period days before it.
|
||||
|
||||
// But wait, the user might have logged Day 1, Day 2, Day 3.
|
||||
// `entry` will be Day 3.
|
||||
// We want Day 1.
|
||||
|
||||
// Let's try a different approach:
|
||||
// Get all period days sorted DESC.
|
||||
final periodDays = sortedEntries.where((e) => e.isPeriodDay).toList();
|
||||
|
||||
if (periodDays.isNotEmpty) {
|
||||
// Take the latest block
|
||||
DateTime latestParams = periodDays.first.date;
|
||||
|
||||
// Now find the "start" of this block
|
||||
// We iterate backwards from the *latest* date found
|
||||
|
||||
DateTime currentSearch = latestParams;
|
||||
DateTime startOfBlock = latestParams;
|
||||
|
||||
// Check if we have an entry for the day before
|
||||
bool foundPrevious = true;
|
||||
while (foundPrevious) {
|
||||
final dayBefore = currentSearch.subtract(const Duration(days: 1));
|
||||
final hasDayBefore =
|
||||
periodDays.any((e) => DateUtils.isSameDay(e.date, dayBefore));
|
||||
if (hasDayBefore) {
|
||||
currentSearch = dayBefore;
|
||||
startOfBlock = dayBefore;
|
||||
} else {
|
||||
foundPrevious = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If this calculated start is more recent than the user profile one, use it
|
||||
if (lastPeriodStart == null ||
|
||||
startOfBlock.isAfter(lastPeriodStart)) {
|
||||
lastPeriodStart = startOfBlock;
|
||||
}
|
||||
}
|
||||
break; // We only care about the most recent period block
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,38 +128,41 @@ class CycleService {
|
||||
isPeriodExpected: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Check if the calculated last period is in the future (invalid state validation)
|
||||
if (lastPeriodStart.isAfter(DateTime.now())) {
|
||||
// Fallback to today if data is weird, or just use it (maybe user logged future?)
|
||||
// Let's stick to standard logic:
|
||||
// Fallback to today if data is weird, or just use it (maybe user logged future?)
|
||||
// Let's stick to standard logic:
|
||||
}
|
||||
|
||||
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 startOfCycle = DateTime(lastPeriodStart.year, lastPeriodStart.month, lastPeriodStart.day);
|
||||
|
||||
final daysSinceLastPeriod = startOfToday.difference(startOfCycle).inDays + 1;
|
||||
|
||||
final startOfCycle = DateTime(
|
||||
lastPeriodStart.year, lastPeriodStart.month, lastPeriodStart.day);
|
||||
|
||||
final daysSinceLastPeriod =
|
||||
startOfToday.difference(startOfCycle).inDays + 1;
|
||||
|
||||
// If negative (future date), handle gracefully
|
||||
if (daysSinceLastPeriod < 1) {
|
||||
return CycleInfo(
|
||||
return CycleInfo(
|
||||
phase: CyclePhase.follicular,
|
||||
dayOfCycle: 1,
|
||||
daysUntilPeriod: cycleLength,
|
||||
isPeriodExpected: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 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 <= user.averagePeriodLength) { // Use variable period length
|
||||
if (dayOfCycle <= user.averagePeriodLength) {
|
||||
// Use variable period length
|
||||
phase = CyclePhase.menstrual;
|
||||
} else if (dayOfCycle <= 13) {
|
||||
phase = CyclePhase.follicular;
|
||||
@@ -180,13 +185,14 @@ class CycleService {
|
||||
if (user == null || user.lastPeriodStartDate == null) return null;
|
||||
|
||||
final lastPeriodStart = user.lastPeriodStartDate!;
|
||||
|
||||
|
||||
// Normalize dates
|
||||
final checkDate = DateTime(date.year, date.month, date.day);
|
||||
final startCycle = DateTime(lastPeriodStart.year, lastPeriodStart.month, lastPeriodStart.day);
|
||||
final startCycle = DateTime(
|
||||
lastPeriodStart.year, lastPeriodStart.month, lastPeriodStart.day);
|
||||
|
||||
final daysDifference = checkDate.difference(startCycle).inDays;
|
||||
|
||||
|
||||
// If date is before the last known period, we can't reliably predict using this simple logic
|
||||
// (though in reality we could project backwards, but let's stick to forward/current)
|
||||
if (daysDifference < 0) return null;
|
||||
@@ -201,7 +207,8 @@ class CycleService {
|
||||
}
|
||||
|
||||
/// Predicts period days for the next [months] months
|
||||
static List<DateTime> predictNextPeriodDays(UserProfile? user, {int months = 12}) {
|
||||
static List<DateTime> predictNextPeriodDays(UserProfile? user,
|
||||
{int months = 12}) {
|
||||
if (user == null || user.lastPeriodStartDate == null) return [];
|
||||
|
||||
final predictedDays = <DateTime>[];
|
||||
@@ -209,12 +216,12 @@ class CycleService {
|
||||
final cycleLength = user.averageCycleLength;
|
||||
final periodLength = user.averagePeriodLength;
|
||||
|
||||
// Start predicting from the NEXT cycle if the current one is finished,
|
||||
// Start predicting from the NEXT cycle if the current one is finished,
|
||||
// or just project out from the last start date.
|
||||
// We want to list all future period days.
|
||||
|
||||
|
||||
DateTime currentCycleStart = lastPeriodStart;
|
||||
|
||||
|
||||
// Project forward for roughly 'months' months
|
||||
// A safe upper bound for loop is months * 30 days
|
||||
final limitDate = DateTime.now().add(Duration(days: months * 30));
|
||||
@@ -227,11 +234,11 @@ class CycleService {
|
||||
predictedDays.add(periodDay);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Move to next cycle
|
||||
currentCycleStart = currentCycleStart.add(Duration(days: cycleLength));
|
||||
}
|
||||
|
||||
|
||||
return predictedDays;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import 'package:health/health.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../models/cycle_entry.dart';
|
||||
|
||||
@@ -10,14 +8,12 @@ class HealthService {
|
||||
HealthService._internal();
|
||||
|
||||
final Health _health = Health();
|
||||
|
||||
|
||||
// ignore: unused_field
|
||||
List<HealthDataType> _requestedTypes = [];
|
||||
|
||||
// TODO: Fix HealthDataType for menstruation in newer health package versions
|
||||
static const List<HealthDataType> _menstruationDataTypes = [
|
||||
// HealthDataType.MENSTRUATION - Not found in recent versions?
|
||||
HealthDataType.STEPS, // Placeholder to avoid compile error
|
||||
HealthDataType.MENSTRUATION_FLOW,
|
||||
];
|
||||
|
||||
Future<bool> requestAuthorization(List<HealthDataType> types) async {
|
||||
@@ -37,9 +33,10 @@ class HealthService {
|
||||
|
||||
Future<bool> writeMenstruationData(List<CycleEntry> entries) async {
|
||||
// This feature is currently disabled until compatible HealthDataType is identified
|
||||
debugPrint("writeMenstruationData: Currently disabled due to package version incompatibility.");
|
||||
debugPrint(
|
||||
"writeMenstruationData: Currently disabled due to package version incompatibility.");
|
||||
return false;
|
||||
|
||||
|
||||
/*
|
||||
final periodEntries = entries.where((entry) => entry.isPeriodDay).toList();
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class NotificationService {
|
||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
|
||||
final DarwinInitializationSettings initializationSettingsDarwin =
|
||||
const DarwinInitializationSettings initializationSettingsDarwin =
|
||||
DarwinInitializationSettings(
|
||||
requestAlertPermission: true,
|
||||
requestBadgePermission: true,
|
||||
@@ -36,10 +36,10 @@ class NotificationService {
|
||||
);
|
||||
|
||||
// Linux initialization (optional, but good for completeness)
|
||||
final LinuxInitializationSettings initializationSettingsLinux =
|
||||
const LinuxInitializationSettings initializationSettingsLinux =
|
||||
LinuxInitializationSettings(defaultActionName: 'Open notification');
|
||||
|
||||
final InitializationSettings initializationSettings =
|
||||
const InitializationSettings initializationSettings =
|
||||
InitializationSettings(
|
||||
android: initializationSettingsAndroid,
|
||||
iOS: initializationSettingsDarwin,
|
||||
@@ -66,7 +66,8 @@ class NotificationService {
|
||||
if (kIsWeb) {
|
||||
// Web platform limitation: Background scheduling is complex.
|
||||
// For this demo/web preview, we'll just log it or rely on the UI confirmation.
|
||||
print('Web Notification Scheduled: $title - $body at $scheduledDate');
|
||||
debugPrint(
|
||||
'Web Notification Scheduled: $title - $body at $scheduledDate');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,7 +101,7 @@ class NotificationService {
|
||||
String? channelName,
|
||||
}) async {
|
||||
if (kIsWeb) {
|
||||
print('Web Local Notification: $title - $body');
|
||||
debugPrint('Web Local Notification: $title - $body');
|
||||
return;
|
||||
}
|
||||
const AndroidNotificationDetails androidNotificationDetails =
|
||||
|
||||
@@ -1,105 +1,109 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
import 'package:printing/printing.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../models/user_profile.dart';
|
||||
import '../models/cycle_entry.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class PdfService {
|
||||
static Future<void> generateCycleReport(UserProfile? user, List<CycleEntry> entries) async {
|
||||
static Future<void> generateCycleReport(
|
||||
UserProfile user, List<CycleEntry> entries) async {
|
||||
final pdf = pw.Document();
|
||||
final font = await PdfGoogleFonts.outfitRegular();
|
||||
final boldFont = await PdfGoogleFonts.outfitBold();
|
||||
|
||||
final logo = pw.MemoryImage(
|
||||
(await rootBundle.load('assets/images/logo.png')).buffer.asUint8List(),
|
||||
);
|
||||
|
||||
// Group entries by month
|
||||
final entriesByMonth = <String, List<CycleEntry>>{};
|
||||
final Map<String, List<CycleEntry>> groupedEntries = {};
|
||||
for (var entry in entries) {
|
||||
final month = DateFormat('MMMM yyyy').format(entry.date);
|
||||
if (!entriesByMonth.containsKey(month)) {
|
||||
entriesByMonth[month] = [];
|
||||
if (!groupedEntries.containsKey(month)) {
|
||||
groupedEntries[month] = [];
|
||||
}
|
||||
entriesByMonth[month]!.add(entry);
|
||||
groupedEntries[month]!.add(entry);
|
||||
}
|
||||
|
||||
// Sort months chronologically (most recent first)
|
||||
final sortedMonths = groupedEntries.keys.toList()
|
||||
..sort((a, b) {
|
||||
final dateA = DateFormat('MMMM yyyy').parse(a);
|
||||
final dateB = DateFormat('MMMM yyyy').parse(b);
|
||||
return dateB.compareTo(dateA);
|
||||
});
|
||||
|
||||
pdf.addPage(
|
||||
pw.MultiPage(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
theme: pw.ThemeData.withFont(
|
||||
base: font,
|
||||
bold: boldFont,
|
||||
),
|
||||
margin: const pw.EdgeInsets.all(32),
|
||||
build: (pw.Context context) {
|
||||
return [
|
||||
pw.Header(
|
||||
level: 0,
|
||||
child: pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
pw.Text('Cycle Report', style: pw.TextStyle(fontSize: 24, fontWeight: pw.FontWeight.bold)),
|
||||
pw.Text(DateFormat.yMMMd().format(DateTime.now()), style: const pw.TextStyle(color: PdfColors.grey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (user != null)
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.only(bottom: 20),
|
||||
child: pw.Column(
|
||||
pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text('Name: ${user.name}'),
|
||||
pw.Text('Average Cycle Length: ${user.averageCycleLength} days'),
|
||||
pw.Text('Average Period Length: ${user.averagePeriodLength} days'),
|
||||
pw.Text('Cycle History Report',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
color: PdfColors.blueGrey900)),
|
||||
pw.Text('Generated for ${user.name}',
|
||||
style: const pw.TextStyle(
|
||||
fontSize: 14, color: PdfColors.blueGrey600)),
|
||||
pw.Text(
|
||||
'Date Range: ${DateFormat('MMM yyyy').format(entries.last.date)} - ${DateFormat('MMM yyyy').format(entries.first.date)}',
|
||||
style: const pw.TextStyle(
|
||||
fontSize: 12, color: PdfColors.blueGrey400)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
...entriesByMonth.entries.map((entry) {
|
||||
final month = entry.key;
|
||||
final monthEntries = entry.value;
|
||||
// Sort by date
|
||||
monthEntries.sort((a, b) => a.date.compareTo(b.date));
|
||||
|
||||
return pw.Column(
|
||||
pw.SizedBox(height: 50, width: 50, child: pw.Image(logo)),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 30),
|
||||
for (var month in sortedMonths) ...[
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.SizedBox(height: 10),
|
||||
pw.Text(month, style: pw.TextStyle(fontSize: 18, fontWeight: pw.FontWeight.bold, color: PdfColors.blueGrey800)),
|
||||
pw.Text(month,
|
||||
style: pw.TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
color: PdfColors.blueGrey800)),
|
||||
pw.SizedBox(height: 5),
|
||||
pw.Table.fromTextArray(
|
||||
pw.TableHelper.fromTextArray(
|
||||
context: context,
|
||||
headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold),
|
||||
headers: ['Date', 'Phase', 'Details', 'Notes'],
|
||||
data: monthEntries.map((e) {
|
||||
final details = <String>[];
|
||||
if (e.isPeriodDay) details.add('Period');
|
||||
if (e.mood != null) details.add('Mood: ${e.mood!.label}');
|
||||
if (e.symptomCount > 0) details.add('${e.symptomCount} symptoms');
|
||||
|
||||
data: groupedEntries[month]!.map((e) {
|
||||
return [
|
||||
DateFormat('d, E').format(e.date),
|
||||
'${e.isPeriodDay ? "Menstrual" : "-"}', // Simplified for report
|
||||
details.join(', '),
|
||||
e.notes ?? '',
|
||||
DateFormat('MMM d').format(e.date),
|
||||
e.isPeriodDay
|
||||
? 'Period'
|
||||
: (e.flowIntensity == FlowIntensity.spotting
|
||||
? 'Spotting'
|
||||
: 'Other'),
|
||||
e.flowIntensity != null
|
||||
? 'Flow: ${e.flowIntensity.toString().split('.').last}'
|
||||
: '-',
|
||||
e.notes ?? '-',
|
||||
];
|
||||
}).toList(),
|
||||
columnWidths: {
|
||||
0: const pw.FlexColumnWidth(1),
|
||||
1: const pw.FlexColumnWidth(1),
|
||||
2: const pw.FlexColumnWidth(2),
|
||||
3: const pw.FlexColumnWidth(2),
|
||||
},
|
||||
),
|
||||
pw.SizedBox(height: 15),
|
||||
pw.SizedBox(height: 20),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
];
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await Printing.sharePdf(bytes: await pdf.save(), filename: 'cycle_report.pdf');
|
||||
await Printing.layoutPdf(
|
||||
onLayout: (PdfPageFormat format) async => pdf.save(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user