Merge branch 'feature/embed-http-server'
This commit is contained in:
commit
0c8bc9bd03
17 changed files with 448 additions and 115 deletions
26
.forgejo/workflows/test.yml
Normal file
26
.forgejo/workflows/test.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
name: run-on-push
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
# - feature/embed-http-server
|
||||
|
||||
jobs:
|
||||
run-script:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Run script 1
|
||||
run: uname -a
|
||||
- name: Run script 2
|
||||
run: date
|
||||
- name: Run script 3
|
||||
run: whoami
|
||||
- name: Run script 4
|
||||
run: pwd
|
||||
- name: Run script 5
|
||||
run: hostname -I
|
||||
- name: Run script 6
|
||||
run: hostname
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run script 1
|
||||
run: ./test.sh
|
||||
9
Makefile
9
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
|
||||
|
|
@ -35,6 +35,13 @@ build-linux-debug-container:
|
|||
build-web-debug-container:
|
||||
$(FLUTTERW) build web --debug
|
||||
|
||||
# Zip web build for embedding as asset
|
||||
assets/generated/webapp.zip: build/web
|
||||
mkdir -p assets/generated
|
||||
cd build/web && zip -r ../../assets/generated/webapp.zip .
|
||||
|
||||
build-webapp-zip: assets/generated/webapp.zip
|
||||
|
||||
run-linux-debug-container:
|
||||
$(FLUTTERW) run -d linux
|
||||
|
||||
|
|
|
|||
4
TODO.md
4
TODO.md
|
|
@ -91,5 +91,7 @@
|
|||
- Add a rule in Makefile to create a zip file with the contents of ./build/web in the ./assets/generated directory -> ./assets/generated/webapp.zip
|
||||
- Add webapp.zip as a flutter asset
|
||||
- In the /static path, serve the files contained in webapp.zip
|
||||
- [ ] Ensure database is loaded if the file exist, and not use default corpus allways.
|
||||
- [X] Ensure database is loaded if the file exist, and not use default corpus allways.
|
||||
- [ ] Ensure more room for formula title in FormulaScreen. Maybe a marquee or another row for buttons or both.
|
||||
- [ ] In android, images in description are not shown.
|
||||
- [ ] Make formulaSolver() asyncronous, and show a CircularProgressIndicator inside the output variable while the formula is being solved. Honor a new optinal parameter "timeout" in formulaSolver, that will throw a TimeoutException.
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
[
|
||||
{"name": "Kelvin", "symbol": "K", "isBase": true},
|
||||
{"name": "kelvin", "symbol": "K", "baseUnit": "Kelvin", "factor": 1},
|
||||
{
|
||||
"name": "Celsius",
|
||||
"symbol": "°C",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:d4rt_formulas/error_handler.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart'; // For Clipboard
|
||||
import 'package:d4rt_formulas/formula_models.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../corpus.dart';
|
||||
import '../set_utils.dart';
|
||||
import 'formula_screen.dart';
|
||||
|
|
@ -22,6 +24,53 @@ class FormulaList extends StatefulWidget {
|
|||
|
||||
@override
|
||||
State<FormulaList> createState() => _FormulaListState();
|
||||
|
||||
static String _formulaAndDependenciesToExportStringLiteral(Formula formula) {
|
||||
final corpus = GetIt.instance.get<Corpus>();
|
||||
final dependencies = corpus.withDependencies(formula);
|
||||
final dependenciesAsMap = dependencies.map((f) => f.toMap()).toList();
|
||||
for( final f in dependenciesAsMap ){
|
||||
f.remove("uuid");
|
||||
}
|
||||
return SetUtils.prettyPrint(dependenciesAsMap);
|
||||
}
|
||||
|
||||
|
||||
static void shareFormula(Formula formula) async {
|
||||
try {
|
||||
final exportString = _formulaAndDependenciesToExportStringLiteral(formula);
|
||||
|
||||
// Share the string
|
||||
await share_plus.SharePlus.instance.share(
|
||||
share_plus.ShareParams(
|
||||
text: exportString,
|
||||
subject: 'Sharing formula: ${formula.name}',
|
||||
),
|
||||
);
|
||||
} catch (e, st) {
|
||||
errorHandler.notify(e, st);
|
||||
}
|
||||
}
|
||||
|
||||
static void copyFormula(BuildContext context, Formula formula) async {
|
||||
try {
|
||||
final exportString = _formulaAndDependenciesToExportStringLiteral(formula);
|
||||
|
||||
// Copy to clipboard
|
||||
await Clipboard.setData(ClipboardData(text: exportString));
|
||||
|
||||
// Show a snackbar to confirm
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Formula and dependencies copied to clipboard!'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
} catch (e, st) {
|
||||
errorHandler.notify(e, st);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class _FormulaListState extends State<FormulaList> {
|
||||
|
|
@ -56,49 +105,6 @@ class _FormulaListState extends State<FormulaList> {
|
|||
}).toList();
|
||||
}
|
||||
|
||||
String _formulaAndDependenciesToExportStringLiteral(Formula formula) {
|
||||
final dependencies = widget.corpus.withDependencies(formula);
|
||||
final dependenciesAsMap = dependencies.map((f) => f.toMap()).toList();
|
||||
for( final f in dependenciesAsMap ){
|
||||
f.remove("uuid");
|
||||
}
|
||||
return SetUtils.prettyPrint(dependenciesAsMap);
|
||||
}
|
||||
|
||||
void _shareFormula(Formula formula) async {
|
||||
try {
|
||||
final exportString = _formulaAndDependenciesToExportStringLiteral(formula);
|
||||
|
||||
// Share the string
|
||||
await share_plus.SharePlus.instance.share(
|
||||
share_plus.ShareParams(
|
||||
text: exportString,
|
||||
subject: 'Sharing formula: ${formula.name}',
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
_showErrorDialog('Error sharing formula: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _copyFormula(Formula formula) async {
|
||||
try {
|
||||
final exportString = _formulaAndDependenciesToExportStringLiteral(formula);
|
||||
|
||||
// Copy to clipboard
|
||||
await Clipboard.setData(ClipboardData(text: exportString));
|
||||
|
||||
// Show a snackbar to confirm
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Formula and dependencies copied to clipboard!'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
_showErrorDialog('Error copying formula: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _showErrorDialog(String message) {
|
||||
showDialog(
|
||||
|
|
@ -150,41 +156,7 @@ class _FormulaListState extends State<FormulaList> {
|
|||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.share),
|
||||
tooltip: 'Share or copy to clipboard',
|
||||
onSelected: (value) {
|
||||
if (value == 'share') {
|
||||
_shareFormula(formula);
|
||||
} else if (value == 'copy') {
|
||||
_copyFormula(formula);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'share',
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.share, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(child: Text('Share', softWrap: false)),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'copy',
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.copy, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(child: Text('Copy to clipboard', softWrap: false)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// TOTHINK: Add buttons here, but I don't know which ones
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// dart
|
||||
import 'package:d4rt_formulas/database/database_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown_plus_latex/flutter_markdown_plus_latex.dart';
|
||||
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||
|
|
@ -7,7 +8,10 @@ import '../formula_models.dart';
|
|||
import '../formula_evaluator.dart';
|
||||
import '../corpus.dart';
|
||||
import '../error_handler.dart';
|
||||
import '../service_locator.dart';
|
||||
import '../value_formatter.dart';
|
||||
import 'd4rt_editing_controller.dart';
|
||||
import 'formula_list.dart';
|
||||
import 'unit_dropdown.dart';
|
||||
import 'formula_editor.dart';
|
||||
|
||||
|
|
@ -133,7 +137,7 @@ class _FormulaScreenState extends State<FormulaScreen> {
|
|||
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();
|
||||
}
|
||||
|
|
@ -156,28 +160,105 @@ class _FormulaScreenState extends State<FormulaScreen> {
|
|||
appBar: AppBar(
|
||||
title: Text(formula.name),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.share),
|
||||
tooltip: 'Share or copy to clipboard',
|
||||
onSelected: (value) {
|
||||
if (value == 'share') {
|
||||
FormulaList.shareFormula(formula.originalFormula);
|
||||
} else if (value == 'copy') {
|
||||
FormulaList.copyFormula(context, formula.originalFormula);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'share',
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.share, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(child: Text('Share', softWrap: false)),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'copy',
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.copy, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(child: Text('Copy to clipboard', softWrap: false)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_forever),
|
||||
onPressed: () {
|
||||
print( "Borrando");
|
||||
showAlertDialog(BuildContext context) {
|
||||
// set up the buttons
|
||||
Widget cancelButton = TextButton(
|
||||
child: Text("Cancel"),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
Widget deleteButton = TextButton(
|
||||
child: Text("Delete"),
|
||||
onPressed: () {
|
||||
widget.corpus.forgetFormula(formula.originalFormula);
|
||||
getDatabase().deleteFormula(formula.originalFormula.uuid);
|
||||
Navigator.of(context)
|
||||
..pop()..pop();
|
||||
},
|
||||
);
|
||||
|
||||
// set up the AlertDialog
|
||||
AlertDialog alert = AlertDialog(
|
||||
title: Text("Delete Formula"),
|
||||
content: Text("Please confirm deletion of formula ${formula.name}"),
|
||||
actions: [
|
||||
cancelButton,
|
||||
deleteButton,
|
||||
],
|
||||
);
|
||||
return alert;
|
||||
}
|
||||
// show the dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: showAlertDialog
|
||||
);
|
||||
},
|
||||
tooltip: "Delete formula"
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: formula is DerivedFormula
|
||||
? null
|
||||
: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
FormulaEditor(
|
||||
formula: formula as Formula,
|
||||
corpus: widget.corpus,
|
||||
onSave: (updatedFormula) {
|
||||
widget.onSave?.call(updatedFormula);
|
||||
setState(() {
|
||||
formula = updatedFormula;
|
||||
});
|
||||
},
|
||||
),
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
FormulaEditor(
|
||||
formula: formula as Formula,
|
||||
corpus: widget.corpus,
|
||||
onSave: (updatedFormula) {
|
||||
widget.onSave?.call(updatedFormula);
|
||||
setState(() {
|
||||
formula = updatedFormula;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: formula is DerivedFormula
|
||||
? 'Cannot edit derived formula'
|
||||
: 'Edit Formula',
|
||||
|
|
@ -353,7 +434,7 @@ class _FormulaScreenState extends State<FormulaScreen> {
|
|||
child: TextFormField(
|
||||
readOnly: true,
|
||||
enabled: true,
|
||||
controller: TextEditingController(text: _result),
|
||||
controller: TextEditingController(text: formatOutput(_result)),
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
filled: true,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -245,13 +246,6 @@ class Corpus{
|
|||
loadFormulas(formulas, replaceOnDuplicates: replaceOnDuplicates, checkUnits: true);
|
||||
}
|
||||
|
||||
/// Loads corpus from database elements
|
||||
static Future<Corpus> fromDatabaseElements(List<FormulaElement> elements) async {
|
||||
final corpus = Corpus();
|
||||
corpus.loadFormulaElements(elements);
|
||||
return corpus;
|
||||
}
|
||||
|
||||
/// Returns the formula, the units of the formula, and all the units from the corpus with the same base unit.
|
||||
List<FormulaElement> withDependencies(Formula formula) {
|
||||
final result = <FormulaElement>{};
|
||||
|
|
@ -281,4 +275,11 @@ class Corpus{
|
|||
return result.toList();
|
||||
}
|
||||
|
||||
void forgetFormula(Formula formula) {
|
||||
for (final tag in formula.tags) {
|
||||
_tags[tag]?.remove(formula);
|
||||
}
|
||||
_allFormulas.remove(formula.uuid);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -415,6 +415,7 @@ Number functionSolver(
|
|||
|
||||
while (iter < maxNewtonIters) {
|
||||
final Number y = f(x);
|
||||
print( "iter: $iter x: $x y: $y");
|
||||
if (y == 0 || y.abs() <= maxDelta) {
|
||||
return x;
|
||||
}
|
||||
|
|
@ -422,7 +423,7 @@ Number functionSolver(
|
|||
final Number dy = numericalDerivative(x);
|
||||
|
||||
if (dy == 0 || dy.abs() < 1e-12) {
|
||||
throw NoSolutionException("Derivative is zero or too small, cannot continue Newton-Raphson.");
|
||||
throw NoSolutionException("Derivative is zero or too small, cannot continue Newton-Raphson: $dy");
|
||||
}
|
||||
|
||||
final Number delta = y / dy;
|
||||
|
|
@ -435,7 +436,7 @@ Number functionSolver(
|
|||
// If step exploded, cap the step to a reasonable multiple of `step`
|
||||
final Number maxStepAllowed = step * 1e6;
|
||||
if ((xNew - x).abs() > maxStepAllowed) {
|
||||
xNew = x + (delta.isNegative ? -maxStepAllowed : maxStepAllowed);
|
||||
xNew = x - (delta.isNegative ? -maxStepAllowed : maxStepAllowed);
|
||||
}
|
||||
|
||||
x = xNew;
|
||||
|
|
@ -446,9 +447,16 @@ Number functionSolver(
|
|||
|
||||
try {
|
||||
return searchNewton();
|
||||
} catch (e) {
|
||||
var approx = searchApproximately(hint, hint + step);
|
||||
return binarySearch(approx[0], approx[1]);
|
||||
} catch (e1) {
|
||||
try {
|
||||
var approx = searchApproximately(hint, hint + step);
|
||||
return binarySearch(approx[0], approx[1]);
|
||||
}
|
||||
catch( e2 ){
|
||||
errorHandler.notify(e1);
|
||||
errorHandler.notify(e2);
|
||||
throw NoSolutionException("Failed to find a root using both Newton-Raphson and approximate search: $e1 -- $e2");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,7 +142,9 @@ Future<Corpus> loadCorpusFromDatabaseOrAssets() async {
|
|||
return defaultCorpus;
|
||||
} else {
|
||||
// Load corpus from database elements
|
||||
return await Corpus.fromDatabaseElements(dbElements);
|
||||
final corpus = Corpus();
|
||||
corpus.loadFormulaElements(dbElements, true);
|
||||
return corpus;
|
||||
}
|
||||
} catch (e, st) {
|
||||
// If there's an error loading from database, fall back to default corpus
|
||||
|
|
|
|||
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}' : '';
|
||||
}
|
||||
26
lib/value_formatter.dart
Normal file
26
lib/value_formatter.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
String? formatOutput(dynamic result) {
|
||||
if (result == null) return null;
|
||||
return result.toString();
|
||||
|
||||
// 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();
|
||||
}
|
||||
16
pubspec.lock
16
pubspec.lock
|
|
@ -17,6 +17,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.4.1"
|
||||
archive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: archive
|
||||
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.9"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -688,6 +696,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ dependencies:
|
|||
collection:
|
||||
share_plus:
|
||||
receive_sharing_intent:
|
||||
archive: ^4.0.9
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
@ -88,6 +89,7 @@ flutter:
|
|||
assets:
|
||||
- assets/units/
|
||||
- assets/formulas/
|
||||
- assets/generated/webapp.zip
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
|
|
|||
4
test.sh
Executable file
4
test.sh
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#|/bin/bash
|
||||
echo Es un test de CI/CD
|
||||
echo date
|
||||
uname -a
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import 'dart:math' as Math;
|
||||
|
||||
import 'package:d4rt_formulas/formula_evaluator.dart';
|
||||
import 'package:d4rt_formulas/formula_models.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'dart:math' as Math;
|
||||
|
||||
|
||||
void main() {
|
||||
|
|
@ -22,6 +23,20 @@ void main() {
|
|||
expect( solution, closeTo(5, 1e-10));
|
||||
});
|
||||
|
||||
|
||||
test("Solve x formula", () {
|
||||
final formula = Formula(
|
||||
name: 'Test x',
|
||||
input: [
|
||||
VariableSpec(name: 'x', unit: 'scalar'),
|
||||
],
|
||||
output: VariableSpec(name: 'y', unit: 'scalar'),
|
||||
d4rtCode: 'y = x;',
|
||||
);
|
||||
|
||||
var solution = formulaSolver(formula, "x", {"y": 123456789}, maxDelta: 1e-10);
|
||||
expect(solution, closeTo(123456789, 1e-10));
|
||||
});
|
||||
});
|
||||
|
||||
group('Native functions', () {
|
||||
|
|
@ -75,5 +90,4 @@ void main() {
|
|||
expect(root, closeTo(Math.log(2), 0.01));
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
|
|
|||
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