From 7b5194d04c053552b6fe404d875a21e319d3e140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Mon, 13 Apr 2026 17:02:08 +0200 Subject: [PATCH] kelvin was duplicated, introduced formatter --- Makefile | 2 +- assets/formulas/thermodynamics.d4rt | 2 +- assets/units/temperature.d4rt.units | 1 - lib/ai/formula_screen.dart | 5 +- lib/corpus.dart | 1 + lib/services/web_app_server.dart | 158 ++++++++++++++++++++++++++++ lib/value_formatter.dart | 25 +++++ test/value_formatter_test.dart | 15 +++ 8 files changed, 204 insertions(+), 5 deletions(-) create mode 100644 lib/services/web_app_server.dart create mode 100644 lib/value_formatter.dart create mode 100644 test/value_formatter_test.dart diff --git a/Makefile b/Makefile index 4f8fdf9..f4a2cb5 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ build-container: clean: flutter clean - [ -f $(DATABASEFILE) ] && rm $(DATABASEFILE) + [ -f $(DATABASEFILE) ] && rm $(DATABASEFILE) || true clean-container: rm -r .build-container-cache diff --git a/assets/formulas/thermodynamics.d4rt b/assets/formulas/thermodynamics.d4rt index 7248302..f5246aa 100644 --- a/assets/formulas/thermodynamics.d4rt +++ b/assets/formulas/thermodynamics.d4rt @@ -19,7 +19,7 @@ This law combines Boyle's, Charles's and Avogadro's laws. """, "input": [ {"name": "n", "unit": "mole"}, - {"name": "T", "unit": "kelvin"}, + {"name": "T", "unit": "Kelvin"}, {"name": "V", "unit": "cubic meter"} ], "output": {"name": "P", "unit": "pascal"}, diff --git a/assets/units/temperature.d4rt.units b/assets/units/temperature.d4rt.units index 61c23c6..5c27bae 100644 --- a/assets/units/temperature.d4rt.units +++ b/assets/units/temperature.d4rt.units @@ -1,6 +1,5 @@ [ {"name": "Kelvin", "symbol": "K", "isBase": true}, - {"name": "kelvin", "symbol": "K", "baseUnit": "Kelvin", "factor": 1}, { "name": "Celsius", "symbol": "°C", diff --git a/lib/ai/formula_screen.dart b/lib/ai/formula_screen.dart index 672d82c..973b0a9 100644 --- a/lib/ai/formula_screen.dart +++ b/lib/ai/formula_screen.dart @@ -7,6 +7,7 @@ import '../formula_models.dart'; import '../formula_evaluator.dart'; import '../corpus.dart'; import '../error_handler.dart'; +import '../value_formatter.dart'; import 'd4rt_editing_controller.dart'; import 'unit_dropdown.dart'; import 'formula_editor.dart'; @@ -133,7 +134,7 @@ class _FormulaScreenState extends State { String? unit = formula.output.unit; if (unit != null && result is Number) { final converted = widget.corpus.convert(result, unit, _selectedOutputUnit!); - _result = converted.toStringAsFixed(2); + _result = formatOutput(converted); } else { _result = result?.toString(); } @@ -353,7 +354,7 @@ class _FormulaScreenState extends State { child: TextFormField( readOnly: true, enabled: true, - controller: TextEditingController(text: _result), + controller: TextEditingController(text: formatOutput(_result)), decoration: const InputDecoration( border: UnderlineInputBorder(), filled: true, diff --git a/lib/corpus.dart b/lib/corpus.dart index aecd35b..4775801 100644 --- a/lib/corpus.dart +++ b/lib/corpus.dart @@ -112,6 +112,7 @@ class Corpus{ throw ArgumentError("Duplicate unit:$unit"); } _allUnits[unit.name] = unit; + _baseToUnits[unit.baseUnit]?.remove(unit.name); _baseToUnits[unit.baseUnit]?.add(unit.name); } } diff --git a/lib/services/web_app_server.dart b/lib/services/web_app_server.dart new file mode 100644 index 0000000..ebc7da5 --- /dev/null +++ b/lib/services/web_app_server.dart @@ -0,0 +1,158 @@ +import 'dart:io'; +import 'package:archive/archive_io.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:path/path.dart' as path; + +/// HTTP server that serves webapp.zip contents at /static path +class WebAppServer { + HttpServer? _server; + final int port; + final Map> _extractedFiles = {}; + + WebAppServer({this.port = 8080}); + + /// Start the HTTP server + Future start() async { + if (_server != null) { + print('WebAppServer already running on port $_server!.port'); + return; + } + + await _extractWebAppZip(); + + _server = await HttpServer.bind(InternetAddress.loopbackIPv4, port); + print('WebAppServer started on http://localhost:${_server!.port}'); + + _server!.listen(_handleRequest); + } + + /// Stop the HTTP server + Future stop() async { + await _server?.close(force: true); + _server = null; + print('WebAppServer stopped'); + } + + /// Extract webapp.zip from assets into memory + Future _extractWebAppZip() async { + try { + // Load the zip file from assets + final zipData = await rootBundle.load('assets/generated/webapp.zip'); + final bytes = zipData.buffer.asUint8List(zipData.offsetInBytes, zipData.lengthInBytes); + + // Decode the ZIP archive + final archive = ZipDecoder().decodeBytes(bytes); + + // Extract all files into memory + _extractedFiles.clear(); + for (final file in archive) { + if (file.isFile && file.size > 0) { + _extractedFiles[file.name] = file.content as List; + } + } + + print('Extracted ${_extractedFiles.length} files from webapp.zip'); + } catch (e, st) { + print('Error extracting webapp.zip: $e'); + print(st); + rethrow; + } + } + + /// Handle incoming HTTP requests + void _handleRequest(HttpRequest request) { + try { + String uriPath = request.uri.path; + + // Only handle /static/* paths + if (uriPath.startsWith('/static')) { + String filePath = uriPath.substring('/static'.length); + if (filePath.startsWith('/')) { + filePath = filePath.substring(1); + } + + // Default to index.html if no file specified or path is /static/ + if (filePath.isEmpty) { + filePath = 'index.html'; + } + + _serveFile(request, filePath); + } else { + _sendNotFound(request, 'Not found'); + } + } catch (e, st) { + print('Error handling request: $e'); + print(st); + _sendError(request, 'Internal server error: $e'); + } + } + + /// Serve a file from the extracted zip + void _serveFile(HttpRequest request, String filePath) { + if (!_extractedFiles.containsKey(filePath)) { + _sendNotFound(request, 'File not found: $filePath'); + return; + } + + List fileData = _extractedFiles[filePath]!; + String contentType = _getContentType(filePath); + + request.response.headers.set('Content-Type', contentType); + request.response.headers.set('Content-Length', fileData.length.toString()); + request.response.add(fileData); + request.response.close(); + } + + /// Get MIME type based on file extension + String _getContentType(String filePath) { + // TODO: CHANGE TO A Map + String ext = path.extension(filePath).toLowerCase(); + switch (ext) { + case '.html': + return 'text/html; charset=utf-8'; + case '.js': + return 'application/javascript; charset=utf-8'; + case '.css': + return 'text/css; charset=utf-8'; + case '.json': + return 'application/json; charset=utf-8'; + case '.wasm': + return 'application/wasm'; + case '.png': + return 'image/png'; + case '.jpg': + case '.jpeg': + return 'image/jpeg'; + case '.gif': + return 'image/gif'; + case '.svg': + return 'image/svg+xml'; + case '.ico': + return 'image/x-icon'; + case '.txt': + return 'text/plain; charset=utf-8'; + default: + return 'application/octet-stream'; + } + } + + void _sendNotFound(HttpRequest request, String message) { + request.response.statusCode = HttpStatus.notFound; + request.response.headers.set('Content-Type', 'text/plain'); + request.response.write(message); + request.response.close(); + } + + void _sendError(HttpRequest request, String message) { + request.response.statusCode = HttpStatus.internalServerError; + request.response.headers.set('Content-Type', 'text/plain'); + request.response.write(message); + request.response.close(); + } + + /// Check if server is running + bool get isRunning => _server != null; + + /// Get server URL + String get url => _server != null ? 'http://localhost:${_server!.port}' : ''; +} diff --git a/lib/value_formatter.dart b/lib/value_formatter.dart new file mode 100644 index 0000000..e65f41f --- /dev/null +++ b/lib/value_formatter.dart @@ -0,0 +1,25 @@ + + +import 'package:flutter/cupertino.dart'; + +String? formatOutput(dynamic result) { + if (result == null) return null; + + // Try to parse as number to format with commas + if (result is num) { + var tooMuchPrecision = result.toStringAsPrecision(21); + var parts = tooMuchPrecision.split("e"); + var exponent = parts.length > 1 ? "e${parts[1]}" : ""; + var endingWithZeroes = parts[0]; + while (endingWithZeroes.endsWith('0') && endingWithZeroes.contains('.')) { + endingWithZeroes = endingWithZeroes.substring(0, endingWithZeroes.length - 1); + } + if( endingWithZeroes.endsWith(".") ){ + endingWithZeroes = endingWithZeroes.substring(0, endingWithZeroes.length -1 ); + } + return endingWithZeroes + exponent; + } + + // Otherwise return raw string + return result.toString(); +} diff --git a/test/value_formatter_test.dart b/test/value_formatter_test.dart new file mode 100644 index 0000000..534f711 --- /dev/null +++ b/test/value_formatter_test.dart @@ -0,0 +1,15 @@ +import 'package:d4rt_formulas/corpus.dart'; +import 'package:d4rt_formulas/defaults/default_corpus.dart'; +import 'package:d4rt_formulas/formula_evaluator.dart'; +import 'package:d4rt_formulas/value_formatter.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Format', () { + test('1 is 1', () { + var s = formatOutput(1.0); + expect(s, "1"); + }); + }); +} +