Implement husband-wife connection dialogue and theme support for learn articles

This commit is contained in:
2026-01-05 17:09:15 -06:00
parent 02d25d0cc7
commit 96655f9a74
36 changed files with 3849 additions and 819 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,16 @@ import '../../models/cycle_entry.dart';
import '../../models/user_profile.dart';
import '../../services/notification_service.dart';
import '../../providers/user_provider.dart';
import '../../widgets/protected_wrapper.dart';
class PadTrackerScreen extends ConsumerStatefulWidget {
const PadTrackerScreen({super.key});
final FlowIntensity? initialFlow;
final bool isSpotting;
const PadTrackerScreen({
super.key,
this.initialFlow,
this.isSpotting = false,
});
@override
ConsumerState<PadTrackerScreen> createState() => _PadTrackerScreenState();
@@ -25,6 +32,10 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
@override
void initState() {
super.initState();
_selectedFlow = widget.isSpotting
? FlowIntensity.spotting
: widget.initialFlow ?? FlowIntensity.medium;
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkInitialPrompt();
});
@@ -125,6 +136,75 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
);
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
_updateTimeSinceChange();
_scheduleReminders(time);
}
}
Future<void> _scheduleReminders(DateTime lastChangeTime) async {
final user = ref.read(userProfileProvider);
if (user == null || !user.isPadTrackingEnabled) return;
final service = NotificationService();
// Cancel previous
await service.cancelNotification(200);
await service.cancelNotification(201);
await service.cancelNotification(202);
await service.cancelNotification(203);
// Calculate target
final hours = _recommendedHours;
final changeTime = lastChangeTime.add(Duration(hours: hours));
final now = DateTime.now();
// 2 Hours Before
if (user.notifyPad2Hours) {
final notifyTime = changeTime.subtract(const Duration(hours: 2));
if (notifyTime.isAfter(now)) {
await service.scheduleNotification(
id: 200,
title: 'Upcoming Pad Change',
body: 'Recommended change in 2 hours.',
scheduledDate: notifyTime
);
}
}
// 1 Hour Before
if (user.notifyPad1Hour) {
final notifyTime = changeTime.subtract(const Duration(hours: 1));
if (notifyTime.isAfter(now)) {
await service.scheduleNotification(
id: 201,
title: 'Upcoming Pad Change',
body: 'Recommended change in 1 hour.',
scheduledDate: notifyTime
);
}
}
// 30 Mins Before
if (user.notifyPad30Mins) {
final notifyTime = changeTime.subtract(const Duration(minutes: 30));
if (notifyTime.isAfter(now)) {
await service.scheduleNotification(
id: 202,
title: 'Upcoming Pad Change',
body: 'Recommended change in 30 minutes.',
scheduledDate: notifyTime
);
}
}
// Change Now
if (user.notifyPadNow) {
if (changeTime.isAfter(now)) {
await service.scheduleNotification(
id: 203,
title: 'Time to Change!',
body: 'It has been $hours hours since your last change.',
scheduledDate: changeTime
);
}
}
}
@@ -137,50 +217,53 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
return user.padSupplies![_activeSupplyIndex!];
}
bool get _shouldShowMismatchWarning {
bool get _shouldShowMismatchWarning {
final supply = _activeSupply;
if (supply == null) return false;
// Spotting is fine with any protection
if (_selectedFlow == FlowIntensity.spotting) return false;
int flowValue = 1;
switch (_selectedFlow) {
case FlowIntensity.light: flowValue = 2; break;
case FlowIntensity.medium: flowValue = 3; break;
case FlowIntensity.heavy: flowValue = 5; break;
default: break;
}
return flowValue > supply.absorbency;
}
int get _recommendedHours {
final supply = _activeSupply;
if (supply == null) return false;
int flowValue = 1;
switch (_selectedFlow) {
case FlowIntensity.spotting: flowValue = 1; break;
case FlowIntensity.light: flowValue = 2; break;
case FlowIntensity.medium: flowValue = 3; break;
case FlowIntensity.heavy: flowValue = 5; break;
if (supply == null) return 6; // Default
final type = supply.type;
if (type == PadType.menstrualCup ||
type == PadType.menstrualDisc ||
type == PadType.periodUnderwear) {
return 12;
}
return flowValue > supply.absorbency;
}
int get _recommendedHours {
final supply = _activeSupply;
if (supply == null) return 6; // Default
final type = supply.type;
if (type == PadType.menstrualCup ||
type == PadType.menstrualDisc ||
type == PadType.periodUnderwear) {
return 12;
}
int baseHours;
switch (_selectedFlow) {
case FlowIntensity.heavy:
baseHours = (type == PadType.super_pad || type == PadType.overnight || type == PadType.tampon_super)
? 4
: 3;
break;
case FlowIntensity.medium:
baseHours = 6;
break;
case FlowIntensity.light:
baseHours = 8;
break;
case FlowIntensity.spotting:
baseHours = 8;
break;
}
int baseHours;
switch (_selectedFlow) {
case FlowIntensity.heavy:
baseHours = (type == PadType.super_pad || type == PadType.overnight || type == PadType.tampon_super)
? 4
: 3;
break;
case FlowIntensity.medium:
baseHours = 6;
break;
case FlowIntensity.light:
baseHours = 8;
break;
case FlowIntensity.spotting:
baseHours = 10; // More generous for spotting
break;
}
int flowValue = 1;
switch (_selectedFlow) {
@@ -221,18 +304,22 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
final supply = _activeSupply;
final user = ref.watch(userProfileProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Pad Tracker'),
centerTitle: true,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Supply Selection at the top as requested
_buildSectionHeader('Current Protection'),
return ProtectedContentWrapper(
title: 'Pad Tracker',
isProtected: user?.isSuppliesProtected ?? false,
userProfile: user,
child: Scaffold(
appBar: AppBar(
title: const Text('Pad Tracker'),
centerTitle: true,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Supply Selection at the top as requested
_buildSectionHeader('Current Protection'),
const SizedBox(height: 12),
GestureDetector(
onTap: _showSupplyPicker,
@@ -467,7 +554,7 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
],
),
),
);
));
}
String _formatDuration(Duration d, UserProfile user) {