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:
|
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
|
||||||
|
|
@ -35,6 +35,13 @@ build-linux-debug-container:
|
||||||
build-web-debug-container:
|
build-web-debug-container:
|
||||||
$(FLUTTERW) build web --debug
|
$(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:
|
run-linux-debug-container:
|
||||||
$(FLUTTERW) run -d linux
|
$(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 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
|
- Add webapp.zip as a flutter asset
|
||||||
- In the /static path, serve the files contained in webapp.zip
|
- 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.
|
- [ ] 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": [
|
"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",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
import 'package:d4rt_formulas/error_handler.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart'; // For Clipboard
|
import 'package:flutter/services.dart'; // For Clipboard
|
||||||
import 'package:d4rt_formulas/formula_models.dart';
|
import 'package:d4rt_formulas/formula_models.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
import '../corpus.dart';
|
import '../corpus.dart';
|
||||||
import '../set_utils.dart';
|
import '../set_utils.dart';
|
||||||
import 'formula_screen.dart';
|
import 'formula_screen.dart';
|
||||||
|
|
@ -22,6 +24,53 @@ class FormulaList extends StatefulWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FormulaList> createState() => _FormulaListState();
|
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> {
|
class _FormulaListState extends State<FormulaList> {
|
||||||
|
|
@ -56,49 +105,6 @@ class _FormulaListState extends State<FormulaList> {
|
||||||
}).toList();
|
}).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) {
|
void _showErrorDialog(String message) {
|
||||||
showDialog(
|
showDialog(
|
||||||
|
|
@ -150,41 +156,7 @@ class _FormulaListState extends State<FormulaList> {
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
PopupMenuButton(
|
// TOTHINK: Add buttons here, but I don't know which ones
|
||||||
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)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// dart
|
// dart
|
||||||
|
import 'package:d4rt_formulas/database/database_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_markdown_plus_latex/flutter_markdown_plus_latex.dart';
|
import 'package:flutter_markdown_plus_latex/flutter_markdown_plus_latex.dart';
|
||||||
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
|
@ -7,7 +8,10 @@ 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 '../service_locator.dart';
|
||||||
|
import '../value_formatter.dart';
|
||||||
import 'd4rt_editing_controller.dart';
|
import 'd4rt_editing_controller.dart';
|
||||||
|
import 'formula_list.dart';
|
||||||
import 'unit_dropdown.dart';
|
import 'unit_dropdown.dart';
|
||||||
import 'formula_editor.dart';
|
import 'formula_editor.dart';
|
||||||
|
|
||||||
|
|
@ -133,7 +137,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();
|
||||||
}
|
}
|
||||||
|
|
@ -156,6 +160,83 @@ class _FormulaScreenState extends State<FormulaScreen> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(formula.name),
|
title: Text(formula.name),
|
||||||
actions: [
|
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(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
onPressed: formula is DerivedFormula
|
onPressed: formula is DerivedFormula
|
||||||
|
|
@ -353,7 +434,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -245,13 +246,6 @@ class Corpus{
|
||||||
loadFormulas(formulas, replaceOnDuplicates: replaceOnDuplicates, checkUnits: true);
|
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.
|
/// 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) {
|
List<FormulaElement> withDependencies(Formula formula) {
|
||||||
final result = <FormulaElement>{};
|
final result = <FormulaElement>{};
|
||||||
|
|
@ -281,4 +275,11 @@ class Corpus{
|
||||||
return result.toList();
|
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) {
|
while (iter < maxNewtonIters) {
|
||||||
final Number y = f(x);
|
final Number y = f(x);
|
||||||
|
print( "iter: $iter x: $x y: $y");
|
||||||
if (y == 0 || y.abs() <= maxDelta) {
|
if (y == 0 || y.abs() <= maxDelta) {
|
||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
|
|
@ -422,7 +423,7 @@ Number functionSolver(
|
||||||
final Number dy = numericalDerivative(x);
|
final Number dy = numericalDerivative(x);
|
||||||
|
|
||||||
if (dy == 0 || dy.abs() < 1e-12) {
|
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;
|
final Number delta = y / dy;
|
||||||
|
|
@ -435,7 +436,7 @@ Number functionSolver(
|
||||||
// If step exploded, cap the step to a reasonable multiple of `step`
|
// If step exploded, cap the step to a reasonable multiple of `step`
|
||||||
final Number maxStepAllowed = step * 1e6;
|
final Number maxStepAllowed = step * 1e6;
|
||||||
if ((xNew - x).abs() > maxStepAllowed) {
|
if ((xNew - x).abs() > maxStepAllowed) {
|
||||||
xNew = x + (delta.isNegative ? -maxStepAllowed : maxStepAllowed);
|
xNew = x - (delta.isNegative ? -maxStepAllowed : maxStepAllowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
x = xNew;
|
x = xNew;
|
||||||
|
|
@ -446,9 +447,16 @@ Number functionSolver(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return searchNewton();
|
return searchNewton();
|
||||||
} catch (e) {
|
} catch (e1) {
|
||||||
|
try {
|
||||||
var approx = searchApproximately(hint, hint + step);
|
var approx = searchApproximately(hint, hint + step);
|
||||||
return binarySearch(approx[0], approx[1]);
|
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;
|
return defaultCorpus;
|
||||||
} else {
|
} else {
|
||||||
// Load corpus from database elements
|
// Load corpus from database elements
|
||||||
return await Corpus.fromDatabaseElements(dbElements);
|
final corpus = Corpus();
|
||||||
|
corpus.loadFormulaElements(dbElements, true);
|
||||||
|
return corpus;
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
// If there's an error loading from database, fall back to default corpus
|
// 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"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.4.1"
|
version: "8.4.1"
|
||||||
|
archive:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.9"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -688,6 +696,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.2"
|
version: "1.5.2"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
provider:
|
provider:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ dependencies:
|
||||||
collection:
|
collection:
|
||||||
share_plus:
|
share_plus:
|
||||||
receive_sharing_intent:
|
receive_sharing_intent:
|
||||||
|
archive: ^4.0.9
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
@ -88,6 +89,7 @@ flutter:
|
||||||
assets:
|
assets:
|
||||||
- assets/units/
|
- assets/units/
|
||||||
- assets/formulas/
|
- assets/formulas/
|
||||||
|
- assets/generated/webapp.zip
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
# 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_evaluator.dart';
|
||||||
import 'package:d4rt_formulas/formula_models.dart';
|
import 'package:d4rt_formulas/formula_models.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'dart:math' as Math;
|
|
||||||
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
@ -22,6 +23,20 @@ void main() {
|
||||||
expect( solution, closeTo(5, 1e-10));
|
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', () {
|
group('Native functions', () {
|
||||||
|
|
@ -75,5 +90,4 @@ void main() {
|
||||||
expect(root, closeTo(Math.log(2), 0.01));
|
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