Refine: Add real-time countdown timer and display settings to Pad Tracker
This commit is contained in:
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user