mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-10 07:07:19 +00:00

Add dialogType parameter in WaterHeaterPresenceSensor and CeilingSensorDialog. Update step parameter in FlushValueSelectorWidget. Update step parameter in FunctionBloc and WaterHeaterFunctions. Update step, unit, min, and max parameters in ACFunction subclasses.
298 lines
9.5 KiB
Dart
298 lines
9.5 KiB
Dart
import 'dart:math';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:syncrow_web/pages/routines/widgets/condition_toggle.dart';
|
|
import 'package:syncrow_web/pages/routines/widgets/slider_value_selector.dart';
|
|
import 'package:syncrow_web/utils/color_manager.dart';
|
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
|
|
|
class CustomRoutinesTextbox extends StatefulWidget {
|
|
final String? currentCondition;
|
|
final String dialogType;
|
|
final (double, double) sliderRange;
|
|
final dynamic displayedValue;
|
|
final dynamic initialValue;
|
|
final void Function(String condition) onConditionChanged;
|
|
final void Function(double value) onTextChanged;
|
|
final String unit;
|
|
final double dividendOfRange;
|
|
final double stepIncreaseAmount;
|
|
final bool withSpecialChar;
|
|
|
|
const CustomRoutinesTextbox({
|
|
required this.dialogType,
|
|
required this.sliderRange,
|
|
required this.displayedValue,
|
|
required this.initialValue,
|
|
required this.onConditionChanged,
|
|
required this.onTextChanged,
|
|
required this.currentCondition,
|
|
required this.unit,
|
|
required this.dividendOfRange,
|
|
required this.stepIncreaseAmount,
|
|
required this.withSpecialChar,
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
State<CustomRoutinesTextbox> createState() => _CustomRoutinesTextboxState();
|
|
}
|
|
|
|
class _CustomRoutinesTextboxState extends State<CustomRoutinesTextbox> {
|
|
late final TextEditingController _controller;
|
|
bool hasError = false;
|
|
String? errorMessage;
|
|
|
|
int getDecimalPlaces(double step) {
|
|
String stepStr = step.toString();
|
|
if (stepStr.contains('.')) {
|
|
List<String> parts = stepStr.split('.');
|
|
String decimalPart = parts[1];
|
|
decimalPart = decimalPart.replaceAll(RegExp(r'0+$'), '');
|
|
return decimalPart.isEmpty ? 0 : decimalPart.length;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount);
|
|
double initialValue;
|
|
if (widget.initialValue != null &&
|
|
widget.initialValue is num &&
|
|
(widget.initialValue as num) == 0) {
|
|
initialValue = 0.0;
|
|
} else {
|
|
initialValue = double.tryParse(widget.displayedValue) ?? 0.0;
|
|
}
|
|
_controller = TextEditingController(
|
|
text: initialValue.toStringAsFixed(decimalPlaces),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _validateInput(String value) {
|
|
final doubleValue = double.tryParse(value);
|
|
if (doubleValue == null) {
|
|
setState(() {
|
|
errorMessage = "Invalid number";
|
|
hasError = true;
|
|
});
|
|
return;
|
|
}
|
|
|
|
final min = widget.sliderRange.$1;
|
|
final max = widget.sliderRange.$2;
|
|
|
|
if (doubleValue < min) {
|
|
setState(() {
|
|
errorMessage = "Value must be at least $min";
|
|
hasError = true;
|
|
});
|
|
} else if (doubleValue > max) {
|
|
setState(() {
|
|
errorMessage = "Value must be at most $max";
|
|
hasError = true;
|
|
});
|
|
} else {
|
|
int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount);
|
|
int factor = pow(10, decimalPlaces).toInt();
|
|
int scaledStep = (widget.stepIncreaseAmount * factor).round();
|
|
int scaledValue = (doubleValue * factor).round();
|
|
|
|
if (scaledValue % scaledStep != 0) {
|
|
setState(() {
|
|
errorMessage = "must be a multiple of ${widget.stepIncreaseAmount}";
|
|
hasError = true;
|
|
});
|
|
} else {
|
|
setState(() {
|
|
errorMessage = null;
|
|
hasError = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(CustomRoutinesTextbox oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (widget.initialValue != oldWidget.initialValue) {
|
|
if (widget.initialValue != null &&
|
|
widget.initialValue is num &&
|
|
(widget.initialValue as num) == 0) {
|
|
int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount);
|
|
_controller.text = 0.0.toStringAsFixed(decimalPlaces);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _correctAndUpdateValue(String value) {
|
|
final doubleValue = double.tryParse(value) ?? 0.0;
|
|
int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount);
|
|
double rounded = (doubleValue / widget.stepIncreaseAmount).round() *
|
|
widget.stepIncreaseAmount;
|
|
rounded = rounded.clamp(widget.sliderRange.$1, widget.sliderRange.$2);
|
|
rounded = double.parse(rounded.toStringAsFixed(decimalPlaces));
|
|
|
|
setState(() {
|
|
hasError = false;
|
|
errorMessage = null;
|
|
});
|
|
|
|
_controller.text = rounded.toStringAsFixed(decimalPlaces);
|
|
_controller.selection = TextSelection.fromPosition(
|
|
TextPosition(offset: _controller.text.length),
|
|
);
|
|
widget.onTextChanged(rounded);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount);
|
|
|
|
List<TextInputFormatter> formatters = [];
|
|
if (decimalPlaces == 0) {
|
|
formatters.add(FilteringTextInputFormatter.digitsOnly);
|
|
} else {
|
|
formatters.add(FilteringTextInputFormatter.allow(
|
|
RegExp(r'^\d*\.?\d{0,' + decimalPlaces.toString() + r'}$'),
|
|
));
|
|
}
|
|
formatters.add(RangeInputFormatter(
|
|
min: widget.sliderRange.$1,
|
|
max: widget.sliderRange.$2,
|
|
));
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
if (widget.dialogType == 'IF')
|
|
ConditionToggle(
|
|
currentCondition: widget.currentCondition,
|
|
onChanged: widget.onConditionChanged,
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 2),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
Text(
|
|
'Step: ${widget.stepIncreaseAmount}',
|
|
style: context.textTheme.bodySmall?.copyWith(
|
|
color: ColorsManager.grayColor,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Center(
|
|
child: Container(
|
|
width: 170,
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF8F8F8),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: hasError
|
|
? Border.all(color: Colors.red, width: 1)
|
|
: Border.all(
|
|
color: ColorsManager.lightGrayBorderColor, width: 1),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: ColorsManager.blackColor.withOpacity(0.05),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextFormField(
|
|
controller: _controller,
|
|
style: context.textTheme.bodyLarge?.copyWith(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: ColorsManager.blackColor,
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
inputFormatters: widget.withSpecialChar == true
|
|
? [FilteringTextInputFormatter.digitsOnly]
|
|
: null,
|
|
decoration: const InputDecoration(
|
|
border: InputBorder.none,
|
|
isDense: true,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
onChanged: _validateInput,
|
|
onFieldSubmitted: _correctAndUpdateValue,
|
|
onTapOutside: (_) =>
|
|
_correctAndUpdateValue(_controller.text),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
widget.unit,
|
|
style: context.textTheme.bodyMedium?.copyWith(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: ColorsManager.vividBlue,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (errorMessage != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 2.0),
|
|
child: Text(
|
|
errorMessage!,
|
|
style: context.textTheme.bodySmall?.copyWith(
|
|
color: Colors.red,
|
|
fontSize: 10,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Min. ${widget.sliderRange.$1.toInt()}${widget.unit}',
|
|
style: context.textTheme.bodySmall?.copyWith(
|
|
color: ColorsManager.grayColor,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
Text(
|
|
'Max. ${widget.sliderRange.$2.toInt()}${widget.unit}',
|
|
style: context.textTheme.bodySmall?.copyWith(
|
|
color: ColorsManager.grayColor,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
);
|
|
}
|
|
}
|