2025-09-10 15:03:21 +00:00
|
|
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter/services.dart';
|
2025-09-16 20:01:34 +00:00
|
|
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
2025-09-10 15:03:21 +00:00
|
|
|
import '../formula_models.dart';
|
|
|
|
|
import '../formula_evaluator.dart';
|
|
|
|
|
import '../corpus.dart';
|
|
|
|
|
import 'unit_dropdown.dart';
|
|
|
|
|
|
|
|
|
|
class FormulaScreen extends StatefulWidget {
|
|
|
|
|
final Formula formula;
|
2025-09-20 14:46:21 +00:00
|
|
|
final Corpus corpus;
|
2025-09-10 15:03:21 +00:00
|
|
|
|
|
|
|
|
const FormulaScreen({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.formula,
|
|
|
|
|
required this.corpus,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<FormulaScreen> createState() => _FormulaScreenState();
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-13 14:31:26 +00:00
|
|
|
//// Start of D4rtEditingController class ////
|
|
|
|
|
class D4rtEditingController extends TextEditingController {
|
|
|
|
|
String? _lastError;
|
|
|
|
|
|
|
|
|
|
String? get lastError => _lastError;
|
|
|
|
|
|
|
|
|
|
D4rtEditingController({String? text}) : super(text: text);
|
|
|
|
|
|
|
|
|
|
bool validate() {
|
|
|
|
|
try {
|
|
|
|
|
FormulaEvaluator.evaluateExpression(text);
|
|
|
|
|
_lastError = null;
|
|
|
|
|
return true;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
_lastError = e.toString();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
set text(String newText) {
|
|
|
|
|
super.text = newText;
|
|
|
|
|
validate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void notifyListeners() {
|
|
|
|
|
validate();
|
|
|
|
|
super.notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//// End of D4rtEditingController class ////
|
|
|
|
|
|
2025-09-10 15:03:21 +00:00
|
|
|
class _FormulaScreenState extends State<FormulaScreen> {
|
|
|
|
|
final _formKey = GlobalKey<FormState>();
|
2025-10-13 14:31:26 +00:00
|
|
|
final Map<String, D4rtEditingController> _inputControllers = {};
|
2025-09-10 15:03:21 +00:00
|
|
|
final Map<String, String?> _selectedUnits = {};
|
|
|
|
|
String? _result;
|
|
|
|
|
String? _selectedOutputUnit;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
2025-09-14 14:40:54 +00:00
|
|
|
// Initialize controllers and units with listeners
|
2025-09-10 15:03:21 +00:00
|
|
|
for (final input in widget.formula.input) {
|
2025-10-13 14:31:26 +00:00
|
|
|
_inputControllers[input.name] = D4rtEditingController();
|
2025-09-14 14:56:13 +00:00
|
|
|
_selectedUnits[input.name] = input.unit;
|
2025-09-14 14:40:54 +00:00
|
|
|
_inputControllers[input.name]!.addListener(_evaluateFormula);
|
2025-09-10 15:03:21 +00:00
|
|
|
}
|
2025-09-14 14:56:13 +00:00
|
|
|
_selectedOutputUnit = widget.formula.output.unit;
|
2025-09-10 15:03:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
2025-09-14 14:40:54 +00:00
|
|
|
// Clean up controllers and listeners
|
2025-09-10 15:03:21 +00:00
|
|
|
for (final controller in _inputControllers.values) {
|
2025-09-14 14:40:54 +00:00
|
|
|
controller.removeListener(_evaluateFormula);
|
2025-09-10 15:03:21 +00:00
|
|
|
controller.dispose();
|
|
|
|
|
}
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _evaluateFormula() {
|
|
|
|
|
if (!_formKey.currentState!.validate()) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
final inputValues = <String, dynamic>{};
|
|
|
|
|
for (final input in widget.formula.input) {
|
2025-10-05 15:25:49 +00:00
|
|
|
final text = _inputControllers[input.name]!.text;
|
|
|
|
|
//final value = double.tryParse(text) ?? 0.0;
|
|
|
|
|
final value = FormulaEvaluator.evaluateExpression(text);
|
2025-09-10 15:03:21 +00:00
|
|
|
|
|
|
|
|
// Convert input to base unit if needed
|
2025-09-14 14:48:45 +00:00
|
|
|
// Always convert from dropdown unit to variable's base unit
|
|
|
|
|
inputValues[input.name] = widget.corpus.convert(
|
|
|
|
|
value,
|
|
|
|
|
_selectedUnits[input.name]!,
|
2025-09-14 14:56:13 +00:00
|
|
|
input.unit,
|
2025-09-14 14:48:45 +00:00
|
|
|
);
|
2025-09-10 15:03:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final evaluator = FormulaEvaluator();
|
|
|
|
|
final result = evaluator.evaluate(widget.formula, inputValues);
|
|
|
|
|
|
|
|
|
|
// Convert output to selected unit if needed
|
2025-09-14 14:56:13 +00:00
|
|
|
if (_selectedOutputUnit != widget.formula.output.unit) {
|
2025-09-10 15:03:21 +00:00
|
|
|
_result = widget.corpus.convert(
|
|
|
|
|
result,
|
2025-09-14 14:56:13 +00:00
|
|
|
widget.formula.output.unit,
|
2025-09-10 15:03:21 +00:00
|
|
|
_selectedOutputUnit!,
|
|
|
|
|
).toStringAsFixed(2);
|
|
|
|
|
} else {
|
|
|
|
|
_result = result.toStringAsFixed(2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setState(() {});
|
2025-09-10 15:03:32 +00:00
|
|
|
} catch (e, stack) {
|
|
|
|
|
debugPrint('Formula evaluation error: $e');
|
|
|
|
|
debugPrint('Stack trace: $stack');
|
|
|
|
|
|
2025-09-10 15:03:21 +00:00
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
|
SnackBar(
|
2025-09-10 15:03:32 +00:00
|
|
|
content: Text('Error: ${e.toString()}\n${stack.toString()}'),
|
2025-09-10 15:03:21 +00:00
|
|
|
backgroundColor: Theme.of(context).colorScheme.error,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Scaffold(
|
|
|
|
|
appBar: AppBar(
|
|
|
|
|
title: Text(widget.formula.name),
|
|
|
|
|
),
|
|
|
|
|
body: Form(
|
|
|
|
|
key: _formKey,
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.all(16.0),
|
2025-09-14 14:40:54 +00:00
|
|
|
child: ListView(
|
2025-09-10 15:03:21 +00:00
|
|
|
children: [
|
2025-09-16 20:01:34 +00:00
|
|
|
_buildDescriptionSection(),
|
2025-09-14 14:40:54 +00:00
|
|
|
_buildInputSection(),
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
_buildOutputSection(),
|
2025-09-10 15:03:21 +00:00
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-16 20:01:34 +00:00
|
|
|
Widget _buildDescriptionSection() {
|
|
|
|
|
if (widget.formula.description == null ||
|
|
|
|
|
widget.formula.description!.isEmpty) {
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'Description',
|
|
|
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.all(8),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Theme.of(context).colorScheme.surfaceVariant,
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
),
|
|
|
|
|
child: MarkdownBody(
|
|
|
|
|
data: widget.formula.description!,
|
|
|
|
|
shrinkWrap: true,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 15:03:21 +00:00
|
|
|
Widget _buildInputSection() {
|
|
|
|
|
return Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'Input Variables',
|
|
|
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
...widget.formula.input.map((variable) => _buildVariableRow(variable)),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildOutputSection() {
|
|
|
|
|
return Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'Result',
|
|
|
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Text(widget.formula.output.name),
|
|
|
|
|
const Spacer(),
|
|
|
|
|
SizedBox(
|
2025-10-05 14:53:46 +00:00
|
|
|
width: 150,
|
2025-09-10 15:03:21 +00:00
|
|
|
child: TextFormField(
|
|
|
|
|
readOnly: true,
|
2025-09-14 14:41:34 +00:00
|
|
|
enabled: false,
|
2025-09-10 15:03:21 +00:00
|
|
|
controller: TextEditingController(text: _result),
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
border: UnderlineInputBorder(),
|
2025-09-14 14:41:34 +00:00
|
|
|
filled: true,
|
2025-09-10 15:03:21 +00:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
UnitDropdown(
|
|
|
|
|
corpus: widget.corpus,
|
|
|
|
|
variable: widget.formula.output,
|
|
|
|
|
selectedUnit: _selectedOutputUnit,
|
|
|
|
|
onUnitChanged: (unit) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_selectedOutputUnit = unit;
|
|
|
|
|
});
|
2025-09-14 14:56:19 +00:00
|
|
|
_evaluateFormula();
|
2025-09-10 15:03:21 +00:00
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildVariableRow(VariableSpec variable) {
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Text(variable.name),
|
|
|
|
|
const Spacer(),
|
|
|
|
|
SizedBox(
|
|
|
|
|
width: 100,
|
|
|
|
|
child: TextFormField(
|
|
|
|
|
controller: _inputControllers[variable.name],
|
|
|
|
|
keyboardType: TextInputType.number,
|
|
|
|
|
inputFormatters: [
|
2025-10-13 07:29:58 +00:00
|
|
|
//FilteringTextInputFormatter.allow(RegExp(r'[0-9\.\-]')),
|
2025-09-10 15:03:21 +00:00
|
|
|
],
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
border: UnderlineInputBorder(),
|
|
|
|
|
),
|
|
|
|
|
validator: (value) {
|
|
|
|
|
if (value == null || value.isEmpty) {
|
|
|
|
|
return 'Required';
|
|
|
|
|
}
|
2025-10-13 14:31:26 +00:00
|
|
|
return _inputControllers[variable.name]!.lastError;
|
2025-09-10 15:03:21 +00:00
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
UnitDropdown(
|
|
|
|
|
corpus: widget.corpus,
|
|
|
|
|
variable: variable,
|
|
|
|
|
selectedUnit: _selectedUnits[variable.name],
|
|
|
|
|
onUnitChanged: (unit) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_selectedUnits[variable.name] = unit;
|
|
|
|
|
});
|
2025-09-14 14:52:15 +00:00
|
|
|
_evaluateFormula();
|
2025-09-10 15:03:21 +00:00
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-09-10 15:03:32 +00:00
|
|
|
}
|