mirror of
https://github.com/Dvorinka/1356.git
synced 2026-06-04 20:12:56 +00:00
small fix, don't worry about it
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum Gender {
|
||||
male,
|
||||
female,
|
||||
nonBinary,
|
||||
preferNotToSay;
|
||||
|
||||
static Gender fromString(String? value) {
|
||||
switch (value) {
|
||||
case 'male':
|
||||
return Gender.male;
|
||||
case 'female':
|
||||
return Gender.female;
|
||||
case 'non_binary':
|
||||
return Gender.nonBinary;
|
||||
case 'prefer_not_to_say':
|
||||
return Gender.preferNotToSay;
|
||||
default:
|
||||
return Gender.preferNotToSay;
|
||||
}
|
||||
}
|
||||
|
||||
String toDatabaseString() {
|
||||
switch (this) {
|
||||
case Gender.male:
|
||||
return 'male';
|
||||
case Gender.female:
|
||||
return 'female';
|
||||
case Gender.nonBinary:
|
||||
return 'non_binary';
|
||||
case Gender.preferNotToSay:
|
||||
return 'prefer_not_to_say';
|
||||
}
|
||||
}
|
||||
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case Gender.male:
|
||||
return 'Male';
|
||||
case Gender.female:
|
||||
return 'Female';
|
||||
case Gender.nonBinary:
|
||||
return 'Non-binary';
|
||||
case Gender.preferNotToSay:
|
||||
return 'Prefer not to say';
|
||||
}
|
||||
}
|
||||
|
||||
String get emoji {
|
||||
switch (this) {
|
||||
case Gender.male:
|
||||
return '👨';
|
||||
case Gender.female:
|
||||
return '👩';
|
||||
case Gender.nonBinary:
|
||||
return '🧑';
|
||||
case Gender.preferNotToSay:
|
||||
return '👤';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum HeightUnit {
|
||||
metric('cm', 'cm'),
|
||||
imperial('ft/in', 'ft/in');
|
||||
|
||||
const HeightUnit(this.code, this.displayName);
|
||||
final String code;
|
||||
final String displayName;
|
||||
}
|
||||
|
||||
enum WeightUnit {
|
||||
metric('kg', 'kg'),
|
||||
imperial('lbs', 'lbs');
|
||||
|
||||
const WeightUnit(this.code, this.displayName);
|
||||
final String code;
|
||||
final String displayName;
|
||||
}
|
||||
|
||||
class UnitConversionUtils {
|
||||
// Height conversions
|
||||
static double cmToInches(double cm) => cm / 2.54;
|
||||
static double inchesToCm(double inches) => inches * 2.54;
|
||||
|
||||
static String cmToFeetInches(double cm) {
|
||||
final totalInches = cmToInches(cm);
|
||||
final feet = (totalInches / 12).floor();
|
||||
final inches = (totalInches % 12).round();
|
||||
return "${feet}'${inches}\"";
|
||||
}
|
||||
|
||||
static double feetInchesToCm(String feetInches) {
|
||||
final parts = feetInches.replaceAll('"', '').replaceAll("'", ' ').split(' ');
|
||||
final feet = double.tryParse(parts[0]) ?? 0;
|
||||
final inches = double.tryParse(parts.length > 1 ? parts[1] : '0') ?? 0;
|
||||
return inchesToCm(feet * 12 + inches);
|
||||
}
|
||||
|
||||
// Weight conversions
|
||||
static double kgToLbs(double kg) => kg * 2.20462;
|
||||
static double lbsToKg(double lbs) => lbs / 2.20462;
|
||||
|
||||
// BMI calculation
|
||||
static double calculateBmi(double weightKg, double heightCm) {
|
||||
if (weightKg <= 0 || heightCm <= 0) return 0;
|
||||
final heightM = heightCm / 100;
|
||||
return weightKg / (heightM * heightM);
|
||||
}
|
||||
|
||||
static String getBmiCategory(double bmi) {
|
||||
if (bmi < 18.5) return 'Underweight';
|
||||
if (bmi < 25) return 'Normal weight';
|
||||
if (bmi < 30) return 'Overweight';
|
||||
return 'Obese';
|
||||
}
|
||||
|
||||
static Color getBmiColor(double bmi) {
|
||||
if (bmi < 18.5) return Colors.blue;
|
||||
if (bmi < 25) return Colors.green;
|
||||
if (bmi < 30) return Colors.orange;
|
||||
return Colors.red;
|
||||
}
|
||||
|
||||
// Age calculation
|
||||
static int calculateAge(DateTime birthDate) {
|
||||
final now = DateTime.now();
|
||||
int age = now.year - birthDate.year;
|
||||
if (now.month < birthDate.month ||
|
||||
(now.month == birthDate.month && now.day < birthDate.day)) {
|
||||
age--;
|
||||
}
|
||||
return age;
|
||||
}
|
||||
|
||||
// Format height for display
|
||||
static String formatHeight(double cm, HeightUnit unit) {
|
||||
switch (unit) {
|
||||
case HeightUnit.metric:
|
||||
return '${cm.toStringAsFixed(1)} cm';
|
||||
case HeightUnit.imperial:
|
||||
return cmToFeetInches(cm);
|
||||
}
|
||||
}
|
||||
|
||||
// Format weight for display
|
||||
static String formatWeight(double kg, WeightUnit unit) {
|
||||
switch (unit) {
|
||||
case WeightUnit.metric:
|
||||
return '${kg.toStringAsFixed(1)} kg';
|
||||
case WeightUnit.imperial:
|
||||
final lbs = kgToLbs(kg);
|
||||
return '${lbs.toStringAsFixed(1)} lbs';
|
||||
}
|
||||
}
|
||||
|
||||
// Parse height from input
|
||||
static double? parseHeight(String input, HeightUnit unit) {
|
||||
try {
|
||||
switch (unit) {
|
||||
case HeightUnit.metric:
|
||||
final value = double.tryParse(input.replaceAll(RegExp(r'[^0-9.]'), ''));
|
||||
return value;
|
||||
case HeightUnit.imperial:
|
||||
return feetInchesToCm(input);
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse weight from input
|
||||
static double? parseWeight(String input, WeightUnit unit) {
|
||||
try {
|
||||
final value = double.tryParse(input.replaceAll(RegExp(r'[^0-9.]'), ''));
|
||||
if (value == null) return null;
|
||||
|
||||
switch (unit) {
|
||||
case WeightUnit.metric:
|
||||
return value;
|
||||
case WeightUnit.imperial:
|
||||
return lbsToKg(value);
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BiometricData {
|
||||
final int? age;
|
||||
final Gender? gender;
|
||||
final double? heightCm;
|
||||
final double? weightKg;
|
||||
final HeightUnit heightUnit;
|
||||
final WeightUnit weightUnit;
|
||||
|
||||
const BiometricData({
|
||||
this.age,
|
||||
this.gender,
|
||||
this.heightCm,
|
||||
this.weightKg,
|
||||
this.heightUnit = HeightUnit.metric,
|
||||
this.weightUnit = WeightUnit.metric,
|
||||
});
|
||||
|
||||
double? get bmi {
|
||||
if (heightCm == null || weightKg == null) return null;
|
||||
return UnitConversionUtils.calculateBmi(weightKg!, heightCm!);
|
||||
}
|
||||
|
||||
String get bmiCategory {
|
||||
final bmiValue = bmi;
|
||||
if (bmiValue == null) return '';
|
||||
return UnitConversionUtils.getBmiCategory(bmiValue);
|
||||
}
|
||||
|
||||
String get formattedHeight {
|
||||
if (heightCm == null) return '';
|
||||
return UnitConversionUtils.formatHeight(heightCm!, heightUnit);
|
||||
}
|
||||
|
||||
String get formattedWeight {
|
||||
if (weightKg == null) return '';
|
||||
return UnitConversionUtils.formatWeight(weightKg!, weightUnit);
|
||||
}
|
||||
|
||||
BiometricData copyWith({
|
||||
int? age,
|
||||
Gender? gender,
|
||||
double? heightCm,
|
||||
double? weightKg,
|
||||
HeightUnit? heightUnit,
|
||||
WeightUnit? weightUnit,
|
||||
}) {
|
||||
return BiometricData(
|
||||
age: age ?? this.age,
|
||||
gender: gender ?? this.gender,
|
||||
heightCm: heightCm ?? this.heightCm,
|
||||
weightKg: weightKg ?? this.weightKg,
|
||||
heightUnit: heightUnit ?? this.heightUnit,
|
||||
weightUnit: weightUnit ?? this.weightUnit,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../utils/unit_conversion_utils.dart';
|
||||
|
||||
class UnitInputField extends StatefulWidget {
|
||||
final String labelText;
|
||||
final IconData prefixIcon;
|
||||
final String helperText;
|
||||
final bool enabled;
|
||||
final ValueChanged<double?> onValueChanged;
|
||||
final ValueChanged<dynamic>? onUnitChanged;
|
||||
final double? initialValue;
|
||||
final bool isHeight;
|
||||
|
||||
const UnitInputField({
|
||||
super.key,
|
||||
required this.labelText,
|
||||
required this.prefixIcon,
|
||||
required this.helperText,
|
||||
this.enabled = true,
|
||||
required this.onValueChanged,
|
||||
this.onUnitChanged,
|
||||
this.initialValue,
|
||||
required this.isHeight,
|
||||
});
|
||||
|
||||
@override
|
||||
State<UnitInputField> createState() => _UnitInputFieldState();
|
||||
}
|
||||
|
||||
class _UnitInputFieldState extends State<UnitInputField> {
|
||||
late TextEditingController _controller;
|
||||
late HeightUnit _selectedHeightUnit;
|
||||
late WeightUnit _selectedWeightUnit;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController();
|
||||
_selectedHeightUnit = HeightUnit.metric;
|
||||
_selectedWeightUnit = WeightUnit.metric;
|
||||
|
||||
// Set initial value if provided
|
||||
if (widget.initialValue != null) {
|
||||
if (widget.isHeight) {
|
||||
_controller.text = widget.initialValue!.toStringAsFixed(1);
|
||||
} else {
|
||||
_controller.text = widget.initialValue!.toStringAsFixed(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onUnitChanged(dynamic unit) {
|
||||
setState(() {
|
||||
if (widget.isHeight) {
|
||||
_selectedHeightUnit = unit as HeightUnit;
|
||||
} else {
|
||||
_selectedWeightUnit = unit as WeightUnit;
|
||||
}
|
||||
});
|
||||
// Notify parent widget of unit change
|
||||
widget.onUnitChanged?.call(unit);
|
||||
_convertAndNotify();
|
||||
}
|
||||
|
||||
void _showUnitSelector() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Select Unit'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: (widget.isHeight ? HeightUnit.values : WeightUnit.values).map((unit) {
|
||||
return RadioListTile<dynamic>(
|
||||
title: Text(widget.isHeight ? (unit as HeightUnit).displayName : (unit as WeightUnit).displayName),
|
||||
value: unit,
|
||||
groupValue: widget.isHeight ? _selectedHeightUnit : _selectedWeightUnit,
|
||||
onChanged: widget.enabled ? (value) {
|
||||
_onUnitChanged(value);
|
||||
Navigator.of(context).pop();
|
||||
} : null,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onTextChanged(String text) {
|
||||
_convertAndNotify();
|
||||
}
|
||||
|
||||
void _convertAndNotify() {
|
||||
final text = _controller.text.trim();
|
||||
if (text.isEmpty) {
|
||||
widget.onValueChanged(null);
|
||||
return;
|
||||
}
|
||||
|
||||
double? valueInCmOrKg;
|
||||
|
||||
if (widget.isHeight) {
|
||||
valueInCmOrKg = UnitConversionUtils.parseHeight(text, _selectedHeightUnit);
|
||||
} else {
|
||||
valueInCmOrKg = UnitConversionUtils.parseWeight(text, _selectedWeightUnit);
|
||||
}
|
||||
|
||||
widget.onValueChanged(valueInCmOrKg);
|
||||
}
|
||||
|
||||
String get _unitDisplayText {
|
||||
if (widget.isHeight) {
|
||||
return _selectedHeightUnit.displayName;
|
||||
} else {
|
||||
return _selectedWeightUnit.displayName;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
// Input field
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: TextFormField(
|
||||
controller: _controller,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
labelText: widget.labelText,
|
||||
prefixIcon: Icon(widget.prefixIcon),
|
||||
helperText: widget.helperText,
|
||||
suffixText: _unitDisplayText,
|
||||
isDense: true, // Make the input field more compact
|
||||
),
|
||||
enabled: widget.enabled,
|
||||
onChanged: widget.enabled ? _onTextChanged : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Unit selector - custom button
|
||||
Container(
|
||||
width: 45,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
onTap: widget.enabled ? () => _showUnitSelector() : null,
|
||||
child: Center(
|
||||
child: Text(
|
||||
_unitDisplayText,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user