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 createState() => _CustomRoutinesTextboxState(); } class _CustomRoutinesTextboxState extends State { late final TextEditingController _controller; bool hasError = false; String? errorMessage; int getDecimalPlaces(double step) { String stepStr = step.toString(); if (stepStr.contains('.')) { List 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 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), ], ); } }