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; } } 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 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), ], ); } }