Refine: Add real-time countdown timer and display settings to Pad Tracker

This commit is contained in:
2026-01-02 18:17:23 -06:00
parent 8772b56f36
commit f54222d26a
4 changed files with 107 additions and 10 deletions

View File

@@ -256,6 +256,12 @@ class UserProfile extends HiveObject {
@HiveField(37, defaultValue: true) @HiveField(37, defaultValue: true)
bool notifyLowSupply; bool notifyLowSupply;
@HiveField(39, defaultValue: true)
bool showPadTimerMinutes;
@HiveField(40, defaultValue: false)
bool showPadTimerSeconds;
UserProfile({ UserProfile({
required this.id, required this.id,
required this.name, required this.name,
@@ -295,6 +301,8 @@ class UserProfile extends HiveObject {
this.notifyLowSupply = true, this.notifyLowSupply = true,
this.lastPadChangeTime, this.lastPadChangeTime,
this.padSupplies, this.padSupplies,
this.showPadTimerMinutes = true,
this.showPadTimerSeconds = false,
}); });
/// Check if user is married /// Check if user is married
@@ -359,6 +367,8 @@ class UserProfile extends HiveObject {
bool? notifyLowSupply, bool? notifyLowSupply,
DateTime? lastPadChangeTime, DateTime? lastPadChangeTime,
List<SupplyItem>? padSupplies, List<SupplyItem>? padSupplies,
bool? showPadTimerMinutes,
bool? showPadTimerSeconds,
}) { }) {
return UserProfile( return UserProfile(
id: id ?? this.id, id: id ?? this.id,
@@ -400,6 +410,8 @@ class UserProfile extends HiveObject {
notifyLowSupply: notifyLowSupply ?? this.notifyLowSupply, notifyLowSupply: notifyLowSupply ?? this.notifyLowSupply,
lastPadChangeTime: lastPadChangeTime ?? this.lastPadChangeTime, lastPadChangeTime: lastPadChangeTime ?? this.lastPadChangeTime,
padSupplies: padSupplies ?? this.padSupplies, padSupplies: padSupplies ?? this.padSupplies,
showPadTimerMinutes: showPadTimerMinutes ?? this.showPadTimerMinutes,
showPadTimerSeconds: showPadTimerSeconds ?? this.showPadTimerSeconds,
); );
} }
} }

View File

@@ -103,13 +103,15 @@ class UserProfileAdapter extends TypeAdapter<UserProfile> {
notifyLowSupply: fields[37] == null ? true : fields[37] as bool, notifyLowSupply: fields[37] == null ? true : fields[37] as bool,
lastPadChangeTime: fields[7] as DateTime?, lastPadChangeTime: fields[7] as DateTime?,
padSupplies: (fields[38] as List?)?.cast<SupplyItem>(), padSupplies: (fields[38] as List?)?.cast<SupplyItem>(),
showPadTimerMinutes: fields[39] == null ? true : fields[39] as bool,
showPadTimerSeconds: fields[40] == null ? false : fields[40] as bool,
); );
} }
@override @override
void write(BinaryWriter writer, UserProfile obj) { void write(BinaryWriter writer, UserProfile obj) {
writer writer
..writeByte(38) ..writeByte(40)
..writeByte(0) ..writeByte(0)
..write(obj.id) ..write(obj.id)
..writeByte(1) ..writeByte(1)
@@ -185,7 +187,11 @@ class UserProfileAdapter extends TypeAdapter<UserProfile> {
..writeByte(36) ..writeByte(36)
..write(obj.notifyPeriodStart) ..write(obj.notifyPeriodStart)
..writeByte(37) ..writeByte(37)
..write(obj.notifyLowSupply); ..write(obj.notifyLowSupply)
..writeByte(39)
..write(obj.showPadTimerMinutes)
..writeByte(40)
..write(obj.showPadTimerSeconds);
} }
@override @override

View File

@@ -38,7 +38,7 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
} }
void _startTimer() { void _startTimer() {
_timer = Timer.periodic(const Duration(minutes: 1), (timer) { _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (mounted) { if (mounted) {
setState(() { setState(() {
_updateTimeSinceChange(); _updateTimeSinceChange();
@@ -342,17 +342,19 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
const SizedBox(height: 8), const SizedBox(height: 8),
if (_timeSinceLastChange != Duration.zero) ...[ if (_timeSinceLastChange != Duration.zero) ...[
Text( Text(
isOverdue _formatRemainingTime(
? '${(-remainingHours).toString()}h overdue' Duration(hours: _recommendedHours) - _timeSinceLastChange,
: '${remainingHours}h ${((_recommendedHours * 60) - _timeSinceLastChange.inMinutes) % 60}m', user!
),
style: GoogleFonts.outfit( style: GoogleFonts.outfit(
fontSize: 32, fontSize: 32,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: isOverdue ? AppColors.rose : AppColors.navyBlue, color: isOverdue ? AppColors.rose : AppColors.navyBlue,
), ),
textAlign: TextAlign.center,
), ),
Text( Text(
'Last changed: ${_formatDuration(_timeSinceLastChange)} ago', 'Last changed: ${_formatDuration(_timeSinceLastChange, user)} ago',
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
), ),
] else ...[ ] else ...[
@@ -468,9 +470,46 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
); );
} }
String _formatDuration(Duration d) { String _formatDuration(Duration d, UserProfile user) {
if (d.inHours > 0) return '${d.inHours}h ${d.inMinutes % 60}m'; final hours = d.inHours;
return '${d.inMinutes}m'; final minutes = d.inMinutes % 60;
final seconds = d.inSeconds % 60;
List<String> parts = [];
if (hours > 0) parts.add('${hours}h');
if (user.showPadTimerMinutes) parts.add('${minutes}m');
if (user.showPadTimerSeconds) parts.add('${seconds}s');
if (parts.isEmpty) {
if (hours == 0 && minutes == 0 && seconds == 0) return 'Just now';
return '${d.inMinutes}m'; // Fallback
}
return parts.join(' ');
}
String _formatRemainingTime(Duration remaining, UserProfile user) {
final isOverdue = remaining.isNegative;
final absRemaining = remaining.abs();
final hours = absRemaining.inHours;
final minutes = absRemaining.inMinutes % 60;
final seconds = absRemaining.inSeconds % 60;
List<String> parts = [];
if (hours > 0) parts.add('${hours}h');
if (user.showPadTimerMinutes) {
parts.add('${minutes}m');
}
if (user.showPadTimerSeconds) {
parts.add('${seconds}s');
}
if (parts.isEmpty) {
return isOverdue ? 'Overdue' : 'Change Now';
}
String timeStr = parts.join(' ');
return isOverdue ? '$timeStr overdue' : timeStr;
} }
Widget _buildSectionHeader(String title) { Widget _buildSectionHeader(String title) {

View File

@@ -26,6 +26,8 @@ class _SuppliesSettingsScreenState extends ConsumerState<SuppliesSettingsScreen>
int _padInventoryCount = 0; int _padInventoryCount = 0;
int _lowInventoryThreshold = 5; int _lowInventoryThreshold = 5;
bool _isAutoInventoryEnabled = true; bool _isAutoInventoryEnabled = true;
bool _showPadTimerMinutes = true;
bool _showPadTimerSeconds = false;
final TextEditingController _brandController = TextEditingController(); final TextEditingController _brandController = TextEditingController();
@override @override
@@ -40,6 +42,8 @@ class _SuppliesSettingsScreenState extends ConsumerState<SuppliesSettingsScreen>
_lowInventoryThreshold = user.lowInventoryThreshold; _lowInventoryThreshold = user.lowInventoryThreshold;
_isAutoInventoryEnabled = user.isAutoInventoryEnabled; _isAutoInventoryEnabled = user.isAutoInventoryEnabled;
_brandController.text = user.padBrand ?? ''; _brandController.text = user.padBrand ?? '';
_showPadTimerMinutes = user.showPadTimerMinutes;
_showPadTimerSeconds = user.showPadTimerSeconds;
} }
} }
@@ -57,6 +61,8 @@ class _SuppliesSettingsScreenState extends ConsumerState<SuppliesSettingsScreen>
typicalFlowIntensity: _typicalFlow, typicalFlowIntensity: _typicalFlow,
isAutoInventoryEnabled: _isAutoInventoryEnabled, isAutoInventoryEnabled: _isAutoInventoryEnabled,
padBrand: _brandController.text.trim().isEmpty ? null : _brandController.text.trim(), padBrand: _brandController.text.trim().isEmpty ? null : _brandController.text.trim(),
showPadTimerMinutes: _showPadTimerMinutes,
showPadTimerSeconds: _showPadTimerSeconds,
); );
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
@@ -173,6 +179,40 @@ class _SuppliesSettingsScreenState extends ConsumerState<SuppliesSettingsScreen>
onChanged: (val) => setState(() => _isAutoInventoryEnabled = val), onChanged: (val) => setState(() => _isAutoInventoryEnabled = val),
activeColor: AppColors.menstrualPhase, activeColor: AppColors.menstrualPhase,
), ),
const Divider(height: 32),
Text(
'Timer Display Settings',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
const SizedBox(height: 8),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(
'Show Minutes',
style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: AppColors.charcoal),
),
value: _showPadTimerMinutes,
onChanged: (val) => setState(() => _showPadTimerMinutes = val),
activeColor: AppColors.menstrualPhase,
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(
'Show Seconds',
style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: AppColors.charcoal),
),
value: _showPadTimerSeconds,
onChanged: (val) => setState(() => _showPadTimerSeconds = val),
activeColor: AppColors.menstrualPhase,
),
], ],
], ],
), ),