kelvin was duplicated, introduced formatter
This commit is contained in:
parent
7f49500db8
commit
7b5194d04c
8 changed files with 204 additions and 5 deletions
2
Makefile
2
Makefile
|
|
@ -10,7 +10,7 @@ build-container:
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
flutter clean
|
flutter clean
|
||||||
[ -f $(DATABASEFILE) ] && rm $(DATABASEFILE)
|
[ -f $(DATABASEFILE) ] && rm $(DATABASEFILE) || true
|
||||||
|
|
||||||
clean-container:
|
clean-container:
|
||||||
rm -r .build-container-cache
|
rm -r .build-container-cache
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ This law combines Boyle's, Charles's and Avogadro's laws.
|
||||||
""",
|
""",
|
||||||
"input": [
|
"input": [
|
||||||
{"name": "n", "unit": "mole"},
|
{"name": "n", "unit": "mole"},
|
||||||
{"name": "T", "unit": "kelvin"},
|
{"name": "T", "unit": "Kelvin"},
|
||||||
{"name": "V", "unit": "cubic meter"}
|
{"name": "V", "unit": "cubic meter"}
|
||||||
],
|
],
|
||||||
"output": {"name": "P", "unit": "pascal"},
|
"output": {"name": "P", "unit": "pascal"},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
[
|
[
|
||||||
{"name": "Kelvin", "symbol": "K", "isBase": true},
|
{"name": "Kelvin", "symbol": "K", "isBase": true},
|
||||||
{"name": "kelvin", "symbol": "K", "baseUnit": "Kelvin", "factor": 1},
|
|
||||||
{
|
{
|
||||||
"name": "Celsius",
|
"name": "Celsius",
|
||||||
"symbol": "°C",
|
"symbol": "°C",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import '../formula_models.dart';
|
||||||
import '../formula_evaluator.dart';
|
import '../formula_evaluator.dart';
|
||||||
import '../corpus.dart';
|
import '../corpus.dart';
|
||||||
import '../error_handler.dart';
|
import '../error_handler.dart';
|
||||||
|
import '../value_formatter.dart';
|
||||||
import 'd4rt_editing_controller.dart';
|
import 'd4rt_editing_controller.dart';
|
||||||
import 'unit_dropdown.dart';
|
import 'unit_dropdown.dart';
|
||||||
import 'formula_editor.dart';
|
import 'formula_editor.dart';
|
||||||
|
|
@ -133,7 +134,7 @@ class _FormulaScreenState extends State<FormulaScreen> {
|
||||||
String? unit = formula.output.unit;
|
String? unit = formula.output.unit;
|
||||||
if (unit != null && result is Number) {
|
if (unit != null && result is Number) {
|
||||||
final converted = widget.corpus.convert(result, unit, _selectedOutputUnit!);
|
final converted = widget.corpus.convert(result, unit, _selectedOutputUnit!);
|
||||||
_result = converted.toStringAsFixed(2);
|
_result = formatOutput(converted);
|
||||||
} else {
|
} else {
|
||||||
_result = result?.toString();
|
_result = result?.toString();
|
||||||
}
|
}
|
||||||
|
|
@ -353,7 +354,7 @@ class _FormulaScreenState extends State<FormulaScreen> {
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
controller: TextEditingController(text: _result),
|
controller: TextEditingController(text: formatOutput(_result)),
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
border: UnderlineInputBorder(),
|
border: UnderlineInputBorder(),
|
||||||
filled: true,
|
filled: true,
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ class Corpus{
|
||||||
throw ArgumentError("Duplicate unit:$unit");
|
throw ArgumentError("Duplicate unit:$unit");
|
||||||
}
|
}
|
||||||
_allUnits[unit.name] = unit;
|
_allUnits[unit.name] = unit;
|
||||||
|
_baseToUnits[unit.baseUnit]?.remove(unit.name);
|
||||||
_baseToUnits[unit.baseUnit]?.add(unit.name);
|
_baseToUnits[unit.baseUnit]?.add(unit.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
158
lib/services/web_app_server.dart
Normal file
158
lib/services/web_app_server.dart
Normal file
|
|
@ -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<String, List<int>> _extractedFiles = {};
|
||||||
|
|
||||||
|
WebAppServer({this.port = 8080});
|
||||||
|
|
||||||
|
/// Start the HTTP server
|
||||||
|
Future<void> 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<void> stop() async {
|
||||||
|
await _server?.close(force: true);
|
||||||
|
_server = null;
|
||||||
|
print('WebAppServer stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract webapp.zip from assets into memory
|
||||||
|
Future<void> _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<int>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<int> 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,String>
|
||||||
|
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}' : '';
|
||||||
|
}
|
||||||
25
lib/value_formatter.dart
Normal file
25
lib/value_formatter.dart
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
15
test/value_formatter_test.dart
Normal file
15
test/value_formatter_test.dart
Normal file
|
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in a new issue