mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-10 07:07:19 +00:00
331 lines
10 KiB
Dart
331 lines
10 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;
|
|
}
|
|
}
|
|
|
|
bool _isInitialized = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initializeController();
|
|
}
|
|
|
|
void _initializeController() {
|
|
final decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount);
|
|
final dynamic initialValue = widget.initialValue;
|
|
double parsedValue;
|
|
|
|
if (initialValue is num) {
|
|
parsedValue = initialValue.toDouble();
|
|
} else if (initialValue is String) {
|
|
parsedValue = double.tryParse(initialValue) ?? widget.sliderRange.$1;
|
|
} else {
|
|
parsedValue = widget.sliderRange.$1;
|
|
}
|
|
|
|
_controller = TextEditingController(
|
|
text: parsedValue.toStringAsFixed(decimalPlaces),
|
|
);
|
|
_isInitialized = true;
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(CustomRoutinesTextbox oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
|
|
if (widget.initialValue != oldWidget.initialValue && _isInitialized) {
|
|
final decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount);
|
|
final dynamic initialValue = widget.initialValue;
|
|
double newValue;
|
|
|
|
if (initialValue is num) {
|
|
newValue = initialValue.toDouble();
|
|
} else if (initialValue is String) {
|
|
newValue = double.tryParse(initialValue) ?? widget.sliderRange.$1;
|
|
} else {
|
|
newValue = widget.sliderRange.$1;
|
|
}
|
|
|
|
final newValueText = newValue.toStringAsFixed(decimalPlaces);
|
|
if (_controller.text != newValueText) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_controller.text = newValueText;
|
|
_controller.selection =
|
|
TextSelection.collapsed(offset: _controller.text.length);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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: [
|
|
FilteringTextInputFormatter.allow(
|
|
widget.withSpecialChar
|
|
? RegExp(r'^-?\d*\.?\d{0,' +
|
|
decimalPlaces.toString() +
|
|
r'}$')
|
|
: RegExp(r'\d+'),
|
|
),
|
|
],
|
|
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: Wrap(
|
|
alignment: WrapAlignment.spaceBetween,
|
|
direction: Axis.horizontal,
|
|
children: [
|
|
Text(
|
|
'Min. ${widget.sliderRange.$1.toInt()}${widget.unit}',
|
|
style: context.textTheme.bodySmall?.copyWith(
|
|
color: ColorsManager.grayColor,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
const SizedBox(
|
|
width: 50,
|
|
),
|
|
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),
|
|
],
|
|
);
|
|
}
|
|
}
|