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

@@ -38,7 +38,7 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
}
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<PadTrackerScreen> {
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<PadTrackerScreen> {
);
}
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<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) {

View File

@@ -26,6 +26,8 @@ class _SuppliesSettingsScreenState extends ConsumerState<SuppliesSettingsScreen>
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<SuppliesSettingsScreen>
_lowInventoryThreshold = user.lowInventoryThreshold;
_isAutoInventoryEnabled = user.isAutoInventoryEnabled;
_brandController.text = user.padBrand ?? '';
_showPadTimerMinutes = user.showPadTimerMinutes;
_showPadTimerSeconds = user.showPadTimerSeconds;
}
}
@@ -57,6 +61,8 @@ class _SuppliesSettingsScreenState extends ConsumerState<SuppliesSettingsScreen>
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<SuppliesSettingsScreen>
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,
),
],
],
),