From f54222d26acaf4041211362fa62e2c00b20e171c Mon Sep 17 00:00:00 2001 From: Sterlen Date: Fri, 2 Jan 2026 18:17:23 -0600 Subject: [PATCH] Refine: Add real-time countdown timer and display settings to Pad Tracker --- lib/models/user_profile.dart | 12 ++++ lib/models/user_profile.g.dart | 10 +++- lib/screens/log/pad_tracker_screen.dart | 55 ++++++++++++++++--- .../settings/supplies_settings_screen.dart | 40 ++++++++++++++ 4 files changed, 107 insertions(+), 10 deletions(-) diff --git a/lib/models/user_profile.dart b/lib/models/user_profile.dart index d86a8bc..7cd2688 100644 --- a/lib/models/user_profile.dart +++ b/lib/models/user_profile.dart @@ -256,6 +256,12 @@ class UserProfile extends HiveObject { @HiveField(37, defaultValue: true) bool notifyLowSupply; + @HiveField(39, defaultValue: true) + bool showPadTimerMinutes; + + @HiveField(40, defaultValue: false) + bool showPadTimerSeconds; + UserProfile({ required this.id, required this.name, @@ -295,6 +301,8 @@ class UserProfile extends HiveObject { this.notifyLowSupply = true, this.lastPadChangeTime, this.padSupplies, + this.showPadTimerMinutes = true, + this.showPadTimerSeconds = false, }); /// Check if user is married @@ -359,6 +367,8 @@ class UserProfile extends HiveObject { bool? notifyLowSupply, DateTime? lastPadChangeTime, List? padSupplies, + bool? showPadTimerMinutes, + bool? showPadTimerSeconds, }) { return UserProfile( id: id ?? this.id, @@ -400,6 +410,8 @@ class UserProfile extends HiveObject { notifyLowSupply: notifyLowSupply ?? this.notifyLowSupply, lastPadChangeTime: lastPadChangeTime ?? this.lastPadChangeTime, padSupplies: padSupplies ?? this.padSupplies, + showPadTimerMinutes: showPadTimerMinutes ?? this.showPadTimerMinutes, + showPadTimerSeconds: showPadTimerSeconds ?? this.showPadTimerSeconds, ); } } diff --git a/lib/models/user_profile.g.dart b/lib/models/user_profile.g.dart index 26c34ec..a824fee 100644 --- a/lib/models/user_profile.g.dart +++ b/lib/models/user_profile.g.dart @@ -103,13 +103,15 @@ class UserProfileAdapter extends TypeAdapter { notifyLowSupply: fields[37] == null ? true : fields[37] as bool, lastPadChangeTime: fields[7] as DateTime?, padSupplies: (fields[38] as List?)?.cast(), + showPadTimerMinutes: fields[39] == null ? true : fields[39] as bool, + showPadTimerSeconds: fields[40] == null ? false : fields[40] as bool, ); } @override void write(BinaryWriter writer, UserProfile obj) { writer - ..writeByte(38) + ..writeByte(40) ..writeByte(0) ..write(obj.id) ..writeByte(1) @@ -185,7 +187,11 @@ class UserProfileAdapter extends TypeAdapter { ..writeByte(36) ..write(obj.notifyPeriodStart) ..writeByte(37) - ..write(obj.notifyLowSupply); + ..write(obj.notifyLowSupply) + ..writeByte(39) + ..write(obj.showPadTimerMinutes) + ..writeByte(40) + ..write(obj.showPadTimerSeconds); } @override diff --git a/lib/screens/log/pad_tracker_screen.dart b/lib/screens/log/pad_tracker_screen.dart index f7e26be..6ca3a9f 100644 --- a/lib/screens/log/pad_tracker_screen.dart +++ b/lib/screens/log/pad_tracker_screen.dart @@ -38,7 +38,7 @@ class _PadTrackerScreenState extends ConsumerState { } void _startTimer() { - _timer = Timer.periodic(const Duration(minutes: 1), (timer) { + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (mounted) { setState(() { _updateTimeSinceChange(); @@ -342,17 +342,19 @@ class _PadTrackerScreenState extends ConsumerState { const SizedBox(height: 8), if (_timeSinceLastChange != Duration.zero) ...[ Text( - isOverdue - ? '${(-remainingHours).toString()}h overdue' - : '${remainingHours}h ${((_recommendedHours * 60) - _timeSinceLastChange.inMinutes) % 60}m', + _formatRemainingTime( + Duration(hours: _recommendedHours) - _timeSinceLastChange, + user! + ), style: GoogleFonts.outfit( fontSize: 32, fontWeight: FontWeight.bold, color: isOverdue ? AppColors.rose : AppColors.navyBlue, ), + textAlign: TextAlign.center, ), Text( - 'Last changed: ${_formatDuration(_timeSinceLastChange)} ago', + 'Last changed: ${_formatDuration(_timeSinceLastChange, user)} ago', style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), ), ] else ...[ @@ -468,9 +470,46 @@ class _PadTrackerScreenState extends ConsumerState { ); } - String _formatDuration(Duration d) { - if (d.inHours > 0) return '${d.inHours}h ${d.inMinutes % 60}m'; - return '${d.inMinutes}m'; + String _formatDuration(Duration d, UserProfile user) { + final hours = d.inHours; + final minutes = d.inMinutes % 60; + final seconds = d.inSeconds % 60; + + List 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 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) { diff --git a/lib/screens/settings/supplies_settings_screen.dart b/lib/screens/settings/supplies_settings_screen.dart index 0ec6c76..6c4d0c2 100644 --- a/lib/screens/settings/supplies_settings_screen.dart +++ b/lib/screens/settings/supplies_settings_screen.dart @@ -26,6 +26,8 @@ class _SuppliesSettingsScreenState extends ConsumerState int _padInventoryCount = 0; int _lowInventoryThreshold = 5; bool _isAutoInventoryEnabled = true; + bool _showPadTimerMinutes = true; + bool _showPadTimerSeconds = false; final TextEditingController _brandController = TextEditingController(); @override @@ -40,6 +42,8 @@ class _SuppliesSettingsScreenState extends ConsumerState _lowInventoryThreshold = user.lowInventoryThreshold; _isAutoInventoryEnabled = user.isAutoInventoryEnabled; _brandController.text = user.padBrand ?? ''; + _showPadTimerMinutes = user.showPadTimerMinutes; + _showPadTimerSeconds = user.showPadTimerSeconds; } } @@ -57,6 +61,8 @@ class _SuppliesSettingsScreenState extends ConsumerState typicalFlowIntensity: _typicalFlow, isAutoInventoryEnabled: _isAutoInventoryEnabled, padBrand: _brandController.text.trim().isEmpty ? null : _brandController.text.trim(), + showPadTimerMinutes: _showPadTimerMinutes, + showPadTimerSeconds: _showPadTimerSeconds, ); await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); @@ -173,6 +179,40 @@ class _SuppliesSettingsScreenState extends ConsumerState onChanged: (val) => setState(() => _isAutoInventoryEnabled = val), 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, + ), ], ], ),