Compare commits
No commits in common. "24005542592618cab8f6416e01306408632b63d3" and "958050311e5ac7d32cfd530aca4d8648695567b8" have entirely different histories.
2400554259
...
958050311e
22 changed files with 252 additions and 838 deletions
9
Makefile
9
Makefile
|
|
@ -10,7 +10,7 @@ build-container:
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
flutter clean
|
flutter clean
|
||||||
[ -f $(DATABASEFILE) ] && rm $(DATABASEFILE) || true
|
[ -f $(DATABASEFILE) ] && rm $(DATABASEFILE)
|
||||||
|
|
||||||
clean-container:
|
clean-container:
|
||||||
rm -r .build-container-cache
|
rm -r .build-container-cache
|
||||||
|
|
@ -35,13 +35,6 @@ 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
|
||||||
|
|
||||||
|
|
|
||||||
18
TODO.md
18
TODO.md
|
|
@ -79,17 +79,7 @@
|
||||||
- It will show a screen with a text editor with dart syntax and a button "paste".
|
- It will show a screen with a text editor with dart syntax and a button "paste".
|
||||||
- The "paste" button will copy the clipboard into the text editor.
|
- The "paste" button will copy the clipboard into the text editor.
|
||||||
- A second button "import" will use the import preview screen
|
- A second button "import" will use the import preview screen
|
||||||
-[X] Launch test app_test.dart. Iterate until the test pass.
|
-[R] Launch test app_test.dart. Iterate until the test pass.
|
||||||
- [R] Add test to app_test.dart: share first formula to clipboard and import it
|
- [ ] Add a uuid column to the table or FormulaElements, so it is not necessary to load all the formulas to find a formula by uuid. This will improve performance when updating and deleting.
|
||||||
- [R] Unify UUID and id of FormulaElement
|
- [ ] Make formulaSolver() asyncronous, and show a CircularProgressIndicator while the formula is being solved. Honor a new optinal parameter "timeout" in formulaSolver, that will throw a TimeoutException.
|
||||||
- UUID is in memory
|
- [ ] When importing FormulaElements, save the FormulaElements in the database (currently, they are only added to the Corpus in memory).
|
||||||
- id is in database
|
|
||||||
- remove id from database, add UUID to database
|
|
||||||
- [X] Solve exception in _CorpusLoaderState.build() when GetIt.instance.registerSingleton<Corpus>(corpus) after importing formula, since there is already registeted.
|
|
||||||
- [R] When importing FormulaElements, save the FormulaElements in the database (currently, they are only added to the Corpus in memory).
|
|
||||||
- [ ] Include an http server in the application (linux and android).
|
|
||||||
- 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.
|
|
||||||
- [ ] 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,5 +1,6 @@
|
||||||
[
|
[
|
||||||
{"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",
|
||||||
|
|
|
||||||
8
flutterw
8
flutterw
|
|
@ -7,8 +7,6 @@ BUILDCACHE=./.build-container-cache
|
||||||
DOCKERFILE=./docker/Dockerfile
|
DOCKERFILE=./docker/Dockerfile
|
||||||
IMAGE=d4rt-formulas-builder
|
IMAGE=d4rt-formulas-builder
|
||||||
|
|
||||||
ALL_ARGS="$@"
|
|
||||||
|
|
||||||
detect_container_manager(){
|
detect_container_manager(){
|
||||||
|
|
||||||
if [ "$DOCKER" != "" ]
|
if [ "$DOCKER" != "" ]
|
||||||
|
|
@ -20,12 +18,6 @@ detect_container_manager(){
|
||||||
elif command -v docker > /dev/null 2>&1
|
elif command -v docker > /dev/null 2>&1
|
||||||
then
|
then
|
||||||
DOCKER=docker
|
DOCKER=docker
|
||||||
elif [ -n "$DISTROBOX_HOST_HOME" ]
|
|
||||||
then
|
|
||||||
echo "Detected distrobox, as DISTROBOX_HOST_HOME is defined"
|
|
||||||
echo "Please try to run this script as: "
|
|
||||||
echo " distrobox-host-exec $0 $ALL_ARGS"
|
|
||||||
exit 3
|
|
||||||
else
|
else
|
||||||
echo "Error: no container manager detected (like 'docker' or 'podman'), please define DOCKER variable"
|
echo "Error: no container manager detected (like 'docker' or 'podman'), please define DOCKER variable"
|
||||||
exit 2
|
exit 2
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
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';
|
||||||
|
|
@ -24,53 +22,6 @@ 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> {
|
||||||
|
|
@ -105,6 +56,49 @@ 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(
|
||||||
|
|
@ -156,7 +150,39 @@ class _FormulaListState extends State<FormulaList> {
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// TOTHINK: Add buttons here, but I don't know which ones
|
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(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.share),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Share'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'copy',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.copy),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Copy to clipboard'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// 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';
|
||||||
|
|
@ -8,10 +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 '../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';
|
||||||
|
|
||||||
|
|
@ -137,7 +133,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 = formatOutput(converted);
|
_result = converted.toStringAsFixed(2);
|
||||||
} else {
|
} else {
|
||||||
_result = result?.toString();
|
_result = result?.toString();
|
||||||
}
|
}
|
||||||
|
|
@ -160,105 +156,28 @@ 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
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
FormulaEditor(
|
FormulaEditor(
|
||||||
formula: formula as Formula,
|
formula: formula as Formula,
|
||||||
corpus: widget.corpus,
|
corpus: widget.corpus,
|
||||||
onSave: (updatedFormula) {
|
onSave: (updatedFormula) {
|
||||||
widget.onSave?.call(updatedFormula);
|
widget.onSave?.call(updatedFormula);
|
||||||
setState(() {
|
setState(() {
|
||||||
formula = updatedFormula;
|
formula = updatedFormula;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
|
||||||
tooltip: formula is DerivedFormula
|
tooltip: formula is DerivedFormula
|
||||||
? 'Cannot edit derived formula'
|
? 'Cannot edit derived formula'
|
||||||
: 'Edit Formula',
|
: 'Edit Formula',
|
||||||
|
|
@ -434,7 +353,7 @@ class _FormulaScreenState extends State<FormulaScreen> {
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
controller: TextEditingController(text: formatOutput(_result)),
|
controller: TextEditingController(text: _result),
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
border: UnderlineInputBorder(),
|
border: UnderlineInputBorder(),
|
||||||
filled: true,
|
filled: true,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import 'package:d4rt_formulas/d4rt_formulas.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:d4rt_formulas/formula_models.dart';
|
import 'package:d4rt_formulas/formula_models.dart';
|
||||||
import 'package:d4rt_formulas/corpus.dart';
|
import 'package:d4rt_formulas/corpus.dart';
|
||||||
import 'package:d4rt_formulas/ai/formula_editor.dart';
|
import 'package:d4rt_formulas/ai/formula_editor.dart';
|
||||||
import 'package:d4rt_formulas/services/import_service.dart';
|
import 'package:d4rt_formulas/services/import_service.dart';
|
||||||
import 'package:d4rt_formulas/service_locator.dart';
|
|
||||||
|
|
||||||
import 'package:flutter_code_editor/flutter_code_editor.dart';
|
import 'package:flutter_code_editor/flutter_code_editor.dart';
|
||||||
import 'package:flutter_highlight/themes/monokai-sublime.dart';
|
import 'package:flutter_highlight/themes/monokai-sublime.dart';
|
||||||
|
|
@ -30,7 +28,11 @@ class _ImportPreviewScreenState extends State<ImportPreviewScreen> {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Select all by default
|
// Select all by default
|
||||||
for (final element in widget.elements) {
|
for (final element in widget.elements) {
|
||||||
_selectedUuids.add(element.uuid);
|
if (element is Formula) {
|
||||||
|
_selectedUuids.add(element.uuid);
|
||||||
|
} else if (element is UnitSpec) {
|
||||||
|
_selectedUuids.add(element.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,9 +59,14 @@ class _ImportPreviewScreenState extends State<ImportPreviewScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _importSelected() async {
|
void _importSelected() {
|
||||||
final selectedElements = widget.elements.where((element) {
|
final selectedElements = widget.elements.where((element) {
|
||||||
return _selectedUuids.contains(element.uuid);
|
if (element is Formula) {
|
||||||
|
return _selectedUuids.contains(element.uuid);
|
||||||
|
} else if (element is UnitSpec) {
|
||||||
|
return _selectedUuids.contains(element.name);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
if (selectedElements.isEmpty) {
|
if (selectedElements.isEmpty) {
|
||||||
|
|
@ -72,19 +79,6 @@ class _ImportPreviewScreenState extends State<ImportPreviewScreen> {
|
||||||
try {
|
try {
|
||||||
widget.corpus.loadFormulaElements(selectedElements, true);
|
widget.corpus.loadFormulaElements(selectedElements, true);
|
||||||
|
|
||||||
// Save imported elements to the database
|
|
||||||
final database = getDatabase();
|
|
||||||
for (final element in selectedElements) {
|
|
||||||
final existingElement = await database.getFormulaElementByUuid(element.uuid);
|
|
||||||
if (existingElement != null) {
|
|
||||||
// Update existing element
|
|
||||||
await database.updateFormulaElement(element.uuid, element.toStringLiteral());
|
|
||||||
} else {
|
|
||||||
// Insert new element
|
|
||||||
await database.insertFormulaElement(element.uuid, element.toStringLiteral());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Imported ${selectedElements.length} element(s) successfully'),
|
content: Text('Imported ${selectedElements.length} element(s) successfully'),
|
||||||
|
|
@ -93,8 +87,7 @@ class _ImportPreviewScreenState extends State<ImportPreviewScreen> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
} catch (e,st) {
|
} catch (e) {
|
||||||
errorHandler.notify('Error importing formula elements: $e', st);
|
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
context,
|
context,
|
||||||
).showSnackBar(SnackBar(content: Text('Error importing: $e'), backgroundColor: Colors.red));
|
).showSnackBar(SnackBar(content: Text('Error importing: $e'), backgroundColor: Colors.red));
|
||||||
|
|
@ -193,7 +186,7 @@ class _ImportPreviewScreenState extends State<ImportPreviewScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildUnitTile(UnitSpec unit) {
|
Widget _buildUnitTile(UnitSpec unit) {
|
||||||
final isSelected = _selectedUuids.contains(unit.uuid);
|
final isSelected = _selectedUuids.contains(unit.name);
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
|
@ -203,9 +196,9 @@ class _ImportPreviewScreenState extends State<ImportPreviewScreen> {
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (value == true) {
|
if (value == true) {
|
||||||
_selectedUuids.add(unit.uuid);
|
_selectedUuids.add(unit.name);
|
||||||
} else {
|
} else {
|
||||||
_selectedUuids.remove(unit.uuid);
|
_selectedUuids.remove(unit.name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,6 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -246,6 +245,13 @@ 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>{};
|
||||||
|
|
@ -275,11 +281,4 @@ 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,39 +31,64 @@ extension CorpusDatabaseExtension on FormulasDatabase {
|
||||||
// Clear existing elements first
|
// Clear existing elements first
|
||||||
await delete(formulaElements).go();
|
await delete(formulaElements).go();
|
||||||
|
|
||||||
// Insert new elements with their UUIDs
|
// Insert new elements
|
||||||
for (final element in elements) {
|
for (final element in elements) {
|
||||||
await insertFormulaElement(element.uuid, element.toStringLiteral());
|
await insertFormulaElement(element.toStringLiteral());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to update a formula in the database by UUID
|
// Method to update a formula in the database by UUID
|
||||||
Future<bool> updateFormula(models.Formula formula) async {
|
Future<bool> updateFormula(models.Formula formula) async {
|
||||||
final existingElement = await getFormulaElementByUuid(formula.uuid);
|
final elements = await getAllFormulaElements();
|
||||||
if (existingElement != null) {
|
|
||||||
await updateFormulaElement(formula.uuid, formula.toStringLiteral());
|
for (final element in elements) {
|
||||||
return true;
|
try {
|
||||||
|
final parsed = SetUtils.parseCorpusElements('[${element.elementText}]');
|
||||||
|
if (parsed.isNotEmpty && parsed.first is models.Formula) {
|
||||||
|
final existingFormula = parsed.first as models.Formula;
|
||||||
|
if (existingFormula.uuid == formula.uuid) {
|
||||||
|
// Update this element
|
||||||
|
await updateFormulaElement(
|
||||||
|
element.id,
|
||||||
|
formula.toStringLiteral()
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error parsing database element during update: $e');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
return false; // Formula not found
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to add a new formula to the database
|
// Method to add a new formula to the database
|
||||||
Future<void> addFormula(models.Formula formula) async {
|
Future<void> addFormula(models.Formula formula) async {
|
||||||
await insertFormulaElement(formula.uuid, formula.toStringLiteral());
|
await insertFormulaElement(formula.toStringLiteral());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to add a new formula element (formula or unit) to the database
|
// Method to delete a formula from the database by name
|
||||||
Future<void> addFormulaElement(models.FormulaElement element) async {
|
|
||||||
await insertFormulaElement(element.uuid, element.toStringLiteral());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method to delete a formula from the database by UUID
|
|
||||||
Future<bool> deleteFormula(String uuid) async {
|
Future<bool> deleteFormula(String uuid) async {
|
||||||
final existingElement = await getFormulaElementByUuid(uuid);
|
final elements = await getAllFormulaElements();
|
||||||
if (existingElement != null) {
|
|
||||||
await deleteFormulaElement(uuid);
|
for (final element in elements) {
|
||||||
return true;
|
try {
|
||||||
|
final parsed = SetUtils.parseCorpusElements('[${element.elementText}]');
|
||||||
|
if (parsed.isNotEmpty && parsed.first is models.Formula) {
|
||||||
|
final existingFormula = parsed.first as models.Formula;
|
||||||
|
if (existingFormula.uuid == uuid) {
|
||||||
|
await deleteFormulaElement(element.id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error parsing database element during delete: $e');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
return false; // Formula not found
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,10 @@ if (dart.library.ffi) 'formulas_database_native.dart';
|
||||||
|
|
||||||
part 'formulas_database.g.dart';
|
part 'formulas_database.g.dart';
|
||||||
|
|
||||||
|
// Define the FORMULAELEMENT table to store both formulas and units as text
|
||||||
class FormulaElements extends Table {
|
class FormulaElements extends Table {
|
||||||
TextColumn get uuid => text()();
|
IntColumn get id => integer().autoIncrement()();
|
||||||
TextColumn get elementText => text()();
|
TextColumn get elementText => text()();
|
||||||
|
|
||||||
@override
|
|
||||||
Set<Column> get primaryKey => {uuid};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DriftDatabase(tables: [FormulaElements])
|
@DriftDatabase(tables: [FormulaElements])
|
||||||
|
|
@ -24,10 +21,8 @@ class FormulasDatabase extends _$FormulasDatabase {
|
||||||
int get schemaVersion => 1;
|
int get schemaVersion => 1;
|
||||||
|
|
||||||
// Method to insert a new formula element (either formula or unit)
|
// Method to insert a new formula element (either formula or unit)
|
||||||
Future<void> insertFormulaElement(String uuid, String elementText) {
|
Future<int> insertFormulaElement(String elementText) {
|
||||||
return into(formulaElements).insert(
|
return into(formulaElements).insert(FormulaElementsCompanion.insert(elementText: elementText));
|
||||||
FormulaElementsCompanion.insert(uuid: uuid, elementText: elementText),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to get all formula elements
|
// Method to get all formula elements
|
||||||
|
|
@ -35,20 +30,20 @@ class FormulasDatabase extends _$FormulasDatabase {
|
||||||
return select(formulaElements).get();
|
return select(formulaElements).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to get a formula element by UUID
|
// Method to get a formula element by ID
|
||||||
Future<FormulaElement?> getFormulaElementByUuid(String uuid) {
|
Future<FormulaElement?> getFormulaElementById(int id) {
|
||||||
return (select(formulaElements)..where((tbl) => tbl.uuid.equals(uuid))).getSingleOrNull();
|
return (select(formulaElements)..where((tbl) => tbl.id.equals(id))).getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to update a formula element
|
// Method to update a formula element
|
||||||
Future<void> updateFormulaElement(String uuid, String newElementText) {
|
Future<void> updateFormulaElement(int id, String newElementText) {
|
||||||
return (update(formulaElements)..where((tbl) => tbl.uuid.equals(uuid)))
|
return (update(formulaElements)..where((tbl) => tbl.id.equals(id)))
|
||||||
.write(FormulaElementsCompanion(elementText: Value(newElementText)));
|
.write(FormulaElementsCompanion.insert(elementText: newElementText));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to delete a formula element
|
// Method to delete a formula element
|
||||||
Future<void> deleteFormulaElement(String uuid) {
|
Future<void> deleteFormulaElement(int id) {
|
||||||
return (delete(formulaElements)..where((tbl) => tbl.uuid.equals(uuid))).go();
|
return (delete(formulaElements)..where((tbl) => tbl.id.equals(id))).go();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional helper methods for direct access to the table
|
// Additional helper methods for direct access to the table
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,18 @@ class $FormulaElementsTable extends FormulaElements
|
||||||
final GeneratedDatabase attachedDatabase;
|
final GeneratedDatabase attachedDatabase;
|
||||||
final String? _alias;
|
final String? _alias;
|
||||||
$FormulaElementsTable(this.attachedDatabase, [this._alias]);
|
$FormulaElementsTable(this.attachedDatabase, [this._alias]);
|
||||||
static const VerificationMeta _uuidMeta = const VerificationMeta('uuid');
|
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||||
@override
|
@override
|
||||||
late final GeneratedColumn<String> uuid = GeneratedColumn<String>(
|
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||||
'uuid',
|
'id',
|
||||||
aliasedName,
|
aliasedName,
|
||||||
false,
|
false,
|
||||||
type: DriftSqlType.string,
|
hasAutoIncrement: true,
|
||||||
requiredDuringInsert: true,
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||||
|
'PRIMARY KEY AUTOINCREMENT',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
static const VerificationMeta _elementTextMeta = const VerificationMeta(
|
static const VerificationMeta _elementTextMeta = const VerificationMeta(
|
||||||
'elementText',
|
'elementText',
|
||||||
|
|
@ -30,7 +34,7 @@ class $FormulaElementsTable extends FormulaElements
|
||||||
requiredDuringInsert: true,
|
requiredDuringInsert: true,
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
List<GeneratedColumn> get $columns => [uuid, elementText];
|
List<GeneratedColumn> get $columns => [id, elementText];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
@override
|
@override
|
||||||
|
|
@ -43,13 +47,8 @@ class $FormulaElementsTable extends FormulaElements
|
||||||
}) {
|
}) {
|
||||||
final context = VerificationContext();
|
final context = VerificationContext();
|
||||||
final data = instance.toColumns(true);
|
final data = instance.toColumns(true);
|
||||||
if (data.containsKey('uuid')) {
|
if (data.containsKey('id')) {
|
||||||
context.handle(
|
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||||
_uuidMeta,
|
|
||||||
uuid.isAcceptableOrUnknown(data['uuid']!, _uuidMeta),
|
|
||||||
);
|
|
||||||
} else if (isInserting) {
|
|
||||||
context.missing(_uuidMeta);
|
|
||||||
}
|
}
|
||||||
if (data.containsKey('element_text')) {
|
if (data.containsKey('element_text')) {
|
||||||
context.handle(
|
context.handle(
|
||||||
|
|
@ -66,14 +65,14 @@ class $FormulaElementsTable extends FormulaElements
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<GeneratedColumn> get $primaryKey => {uuid};
|
Set<GeneratedColumn> get $primaryKey => {id};
|
||||||
@override
|
@override
|
||||||
FormulaElement map(Map<String, dynamic> data, {String? tablePrefix}) {
|
FormulaElement map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
return FormulaElement(
|
return FormulaElement(
|
||||||
uuid: attachedDatabase.typeMapping.read(
|
id: attachedDatabase.typeMapping.read(
|
||||||
DriftSqlType.string,
|
DriftSqlType.int,
|
||||||
data['${effectivePrefix}uuid'],
|
data['${effectivePrefix}id'],
|
||||||
)!,
|
)!,
|
||||||
elementText: attachedDatabase.typeMapping.read(
|
elementText: attachedDatabase.typeMapping.read(
|
||||||
DriftSqlType.string,
|
DriftSqlType.string,
|
||||||
|
|
@ -89,20 +88,20 @@ class $FormulaElementsTable extends FormulaElements
|
||||||
}
|
}
|
||||||
|
|
||||||
class FormulaElement extends DataClass implements Insertable<FormulaElement> {
|
class FormulaElement extends DataClass implements Insertable<FormulaElement> {
|
||||||
final String uuid;
|
final int id;
|
||||||
final String elementText;
|
final String elementText;
|
||||||
const FormulaElement({required this.uuid, required this.elementText});
|
const FormulaElement({required this.id, required this.elementText});
|
||||||
@override
|
@override
|
||||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
final map = <String, Expression>{};
|
final map = <String, Expression>{};
|
||||||
map['uuid'] = Variable<String>(uuid);
|
map['id'] = Variable<int>(id);
|
||||||
map['element_text'] = Variable<String>(elementText);
|
map['element_text'] = Variable<String>(elementText);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
FormulaElementsCompanion toCompanion(bool nullToAbsent) {
|
FormulaElementsCompanion toCompanion(bool nullToAbsent) {
|
||||||
return FormulaElementsCompanion(
|
return FormulaElementsCompanion(
|
||||||
uuid: Value(uuid),
|
id: Value(id),
|
||||||
elementText: Value(elementText),
|
elementText: Value(elementText),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +112,7 @@ class FormulaElement extends DataClass implements Insertable<FormulaElement> {
|
||||||
}) {
|
}) {
|
||||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return FormulaElement(
|
return FormulaElement(
|
||||||
uuid: serializer.fromJson<String>(json['uuid']),
|
id: serializer.fromJson<int>(json['id']),
|
||||||
elementText: serializer.fromJson<String>(json['elementText']),
|
elementText: serializer.fromJson<String>(json['elementText']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -121,19 +120,18 @@ class FormulaElement extends DataClass implements Insertable<FormulaElement> {
|
||||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'uuid': serializer.toJson<String>(uuid),
|
'id': serializer.toJson<int>(id),
|
||||||
'elementText': serializer.toJson<String>(elementText),
|
'elementText': serializer.toJson<String>(elementText),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
FormulaElement copyWith({String? uuid, String? elementText}) =>
|
FormulaElement copyWith({int? id, String? elementText}) => FormulaElement(
|
||||||
FormulaElement(
|
id: id ?? this.id,
|
||||||
uuid: uuid ?? this.uuid,
|
elementText: elementText ?? this.elementText,
|
||||||
elementText: elementText ?? this.elementText,
|
);
|
||||||
);
|
|
||||||
FormulaElement copyWithCompanion(FormulaElementsCompanion data) {
|
FormulaElement copyWithCompanion(FormulaElementsCompanion data) {
|
||||||
return FormulaElement(
|
return FormulaElement(
|
||||||
uuid: data.uuid.present ? data.uuid.value : this.uuid,
|
id: data.id.present ? data.id.value : this.id,
|
||||||
elementText: data.elementText.present
|
elementText: data.elementText.present
|
||||||
? data.elementText.value
|
? data.elementText.value
|
||||||
: this.elementText,
|
: this.elementText,
|
||||||
|
|
@ -143,82 +141,70 @@ class FormulaElement extends DataClass implements Insertable<FormulaElement> {
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return (StringBuffer('FormulaElement(')
|
return (StringBuffer('FormulaElement(')
|
||||||
..write('uuid: $uuid, ')
|
..write('id: $id, ')
|
||||||
..write('elementText: $elementText')
|
..write('elementText: $elementText')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(uuid, elementText);
|
int get hashCode => Object.hash(id, elementText);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
(other is FormulaElement &&
|
(other is FormulaElement &&
|
||||||
other.uuid == this.uuid &&
|
other.id == this.id &&
|
||||||
other.elementText == this.elementText);
|
other.elementText == this.elementText);
|
||||||
}
|
}
|
||||||
|
|
||||||
class FormulaElementsCompanion extends UpdateCompanion<FormulaElement> {
|
class FormulaElementsCompanion extends UpdateCompanion<FormulaElement> {
|
||||||
final Value<String> uuid;
|
final Value<int> id;
|
||||||
final Value<String> elementText;
|
final Value<String> elementText;
|
||||||
final Value<int> rowid;
|
|
||||||
const FormulaElementsCompanion({
|
const FormulaElementsCompanion({
|
||||||
this.uuid = const Value.absent(),
|
this.id = const Value.absent(),
|
||||||
this.elementText = const Value.absent(),
|
this.elementText = const Value.absent(),
|
||||||
this.rowid = const Value.absent(),
|
|
||||||
});
|
});
|
||||||
FormulaElementsCompanion.insert({
|
FormulaElementsCompanion.insert({
|
||||||
required String uuid,
|
this.id = const Value.absent(),
|
||||||
required String elementText,
|
required String elementText,
|
||||||
this.rowid = const Value.absent(),
|
}) : elementText = Value(elementText);
|
||||||
}) : uuid = Value(uuid),
|
|
||||||
elementText = Value(elementText);
|
|
||||||
static Insertable<FormulaElement> custom({
|
static Insertable<FormulaElement> custom({
|
||||||
Expression<String>? uuid,
|
Expression<int>? id,
|
||||||
Expression<String>? elementText,
|
Expression<String>? elementText,
|
||||||
Expression<int>? rowid,
|
|
||||||
}) {
|
}) {
|
||||||
return RawValuesInsertable({
|
return RawValuesInsertable({
|
||||||
if (uuid != null) 'uuid': uuid,
|
if (id != null) 'id': id,
|
||||||
if (elementText != null) 'element_text': elementText,
|
if (elementText != null) 'element_text': elementText,
|
||||||
if (rowid != null) 'rowid': rowid,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
FormulaElementsCompanion copyWith({
|
FormulaElementsCompanion copyWith({
|
||||||
Value<String>? uuid,
|
Value<int>? id,
|
||||||
Value<String>? elementText,
|
Value<String>? elementText,
|
||||||
Value<int>? rowid,
|
|
||||||
}) {
|
}) {
|
||||||
return FormulaElementsCompanion(
|
return FormulaElementsCompanion(
|
||||||
uuid: uuid ?? this.uuid,
|
id: id ?? this.id,
|
||||||
elementText: elementText ?? this.elementText,
|
elementText: elementText ?? this.elementText,
|
||||||
rowid: rowid ?? this.rowid,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
final map = <String, Expression>{};
|
final map = <String, Expression>{};
|
||||||
if (uuid.present) {
|
if (id.present) {
|
||||||
map['uuid'] = Variable<String>(uuid.value);
|
map['id'] = Variable<int>(id.value);
|
||||||
}
|
}
|
||||||
if (elementText.present) {
|
if (elementText.present) {
|
||||||
map['element_text'] = Variable<String>(elementText.value);
|
map['element_text'] = Variable<String>(elementText.value);
|
||||||
}
|
}
|
||||||
if (rowid.present) {
|
|
||||||
map['rowid'] = Variable<int>(rowid.value);
|
|
||||||
}
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return (StringBuffer('FormulaElementsCompanion(')
|
return (StringBuffer('FormulaElementsCompanion(')
|
||||||
..write('uuid: $uuid, ')
|
..write('id: $id, ')
|
||||||
..write('elementText: $elementText, ')
|
..write('elementText: $elementText')
|
||||||
..write('rowid: $rowid')
|
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
@ -239,15 +225,13 @@ abstract class _$FormulasDatabase extends GeneratedDatabase {
|
||||||
|
|
||||||
typedef $$FormulaElementsTableCreateCompanionBuilder =
|
typedef $$FormulaElementsTableCreateCompanionBuilder =
|
||||||
FormulaElementsCompanion Function({
|
FormulaElementsCompanion Function({
|
||||||
required String uuid,
|
Value<int> id,
|
||||||
required String elementText,
|
required String elementText,
|
||||||
Value<int> rowid,
|
|
||||||
});
|
});
|
||||||
typedef $$FormulaElementsTableUpdateCompanionBuilder =
|
typedef $$FormulaElementsTableUpdateCompanionBuilder =
|
||||||
FormulaElementsCompanion Function({
|
FormulaElementsCompanion Function({
|
||||||
Value<String> uuid,
|
Value<int> id,
|
||||||
Value<String> elementText,
|
Value<String> elementText,
|
||||||
Value<int> rowid,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
class $$FormulaElementsTableFilterComposer
|
class $$FormulaElementsTableFilterComposer
|
||||||
|
|
@ -259,8 +243,8 @@ class $$FormulaElementsTableFilterComposer
|
||||||
super.$addJoinBuilderToRootComposer,
|
super.$addJoinBuilderToRootComposer,
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
});
|
});
|
||||||
ColumnFilters<String> get uuid => $composableBuilder(
|
ColumnFilters<int> get id => $composableBuilder(
|
||||||
column: $table.uuid,
|
column: $table.id,
|
||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -279,8 +263,8 @@ class $$FormulaElementsTableOrderingComposer
|
||||||
super.$addJoinBuilderToRootComposer,
|
super.$addJoinBuilderToRootComposer,
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
});
|
});
|
||||||
ColumnOrderings<String> get uuid => $composableBuilder(
|
ColumnOrderings<int> get id => $composableBuilder(
|
||||||
column: $table.uuid,
|
column: $table.id,
|
||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -299,8 +283,8 @@ class $$FormulaElementsTableAnnotationComposer
|
||||||
super.$addJoinBuilderToRootComposer,
|
super.$addJoinBuilderToRootComposer,
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
});
|
});
|
||||||
GeneratedColumn<String> get uuid =>
|
GeneratedColumn<int> get id =>
|
||||||
$composableBuilder(column: $table.uuid, builder: (column) => column);
|
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||||
|
|
||||||
GeneratedColumn<String> get elementText => $composableBuilder(
|
GeneratedColumn<String> get elementText => $composableBuilder(
|
||||||
column: $table.elementText,
|
column: $table.elementText,
|
||||||
|
|
@ -345,23 +329,16 @@ class $$FormulaElementsTableTableManager
|
||||||
$$FormulaElementsTableAnnotationComposer($db: db, $table: table),
|
$$FormulaElementsTableAnnotationComposer($db: db, $table: table),
|
||||||
updateCompanionCallback:
|
updateCompanionCallback:
|
||||||
({
|
({
|
||||||
Value<String> uuid = const Value.absent(),
|
Value<int> id = const Value.absent(),
|
||||||
Value<String> elementText = const Value.absent(),
|
Value<String> elementText = const Value.absent(),
|
||||||
Value<int> rowid = const Value.absent(),
|
}) => FormulaElementsCompanion(id: id, elementText: elementText),
|
||||||
}) => FormulaElementsCompanion(
|
|
||||||
uuid: uuid,
|
|
||||||
elementText: elementText,
|
|
||||||
rowid: rowid,
|
|
||||||
),
|
|
||||||
createCompanionCallback:
|
createCompanionCallback:
|
||||||
({
|
({
|
||||||
required String uuid,
|
Value<int> id = const Value.absent(),
|
||||||
required String elementText,
|
required String elementText,
|
||||||
Value<int> rowid = const Value.absent(),
|
|
||||||
}) => FormulaElementsCompanion.insert(
|
}) => FormulaElementsCompanion.insert(
|
||||||
uuid: uuid,
|
id: id,
|
||||||
elementText: elementText,
|
elementText: elementText,
|
||||||
rowid: rowid,
|
|
||||||
),
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
withReferenceMapper: (p0) => p0
|
||||||
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||||
|
|
|
||||||
|
|
@ -415,7 +415,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -423,7 +422,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: $dy");
|
throw NoSolutionException("Derivative is zero or too small, cannot continue Newton-Raphson.");
|
||||||
}
|
}
|
||||||
|
|
||||||
final Number delta = y / dy;
|
final Number delta = y / dy;
|
||||||
|
|
@ -436,7 +435,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;
|
||||||
|
|
@ -447,16 +446,9 @@ Number functionSolver(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return searchNewton();
|
return searchNewton();
|
||||||
} catch (e1) {
|
} catch (e) {
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,8 @@ import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
typedef Number = double;
|
typedef Number = double;
|
||||||
|
|
||||||
String _generateUuidV4() => Uuid().v4();
|
|
||||||
|
|
||||||
/// Abstract base class for formula elements
|
/// Abstract base class for formula elements
|
||||||
abstract class FormulaElement {
|
abstract class FormulaElement {
|
||||||
String get uuid;
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap();
|
Map<String, dynamic> toMap();
|
||||||
|
|
||||||
String toStringLiteral() {
|
String toStringLiteral() {
|
||||||
|
|
@ -22,8 +18,6 @@ abstract class FormulaElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
class UnitSpec extends FormulaElement {
|
class UnitSpec extends FormulaElement {
|
||||||
@override
|
|
||||||
final String uuid;
|
|
||||||
final String name;
|
final String name;
|
||||||
final String baseUnit;
|
final String baseUnit;
|
||||||
final String symbol;
|
final String symbol;
|
||||||
|
|
@ -34,7 +28,6 @@ class UnitSpec extends FormulaElement {
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
return {
|
||||||
'uuid': uuid,
|
|
||||||
"name": name,
|
"name": name,
|
||||||
"baseUnit": baseUnit,
|
"baseUnit": baseUnit,
|
||||||
"symbol": symbol,
|
"symbol": symbol,
|
||||||
|
|
@ -45,35 +38,32 @@ class UnitSpec extends FormulaElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
UnitSpec({
|
UnitSpec({
|
||||||
String? uuid,
|
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.baseUnit,
|
required this.baseUnit,
|
||||||
required this.symbol,
|
required this.symbol,
|
||||||
this.factorFromUnitToBase,
|
this.factorFromUnitToBase,
|
||||||
this.codeFromBaseToUnit,
|
this.codeFromBaseToUnit,
|
||||||
this.codeFromUnitToBase,
|
this.codeFromUnitToBase,
|
||||||
}) : uuid = uuid ?? _generateUuidV4();
|
});
|
||||||
|
|
||||||
factory UnitSpec.fromSet(Map<Object?, Object?> theSet) {
|
factory UnitSpec.fromSet(Map<Object?, Object?> theSet) {
|
||||||
String? uuid = theSet['uuid'] as String?;
|
|
||||||
String name = SetUtils.stringValue(theSet, "name");
|
String name = SetUtils.stringValue(theSet, "name");
|
||||||
String symbol = SetUtils.stringValue(theSet, "symbol");
|
String symbol = SetUtils.stringValue(theSet, "symbol");
|
||||||
|
|
||||||
if (theSet.containsKey("isBase")) {
|
if (theSet.containsKey("isBase")) {
|
||||||
return UnitSpec(uuid: uuid, name: name, baseUnit: name, symbol: symbol, factorFromUnitToBase: 1);
|
return UnitSpec(name: name, baseUnit: name, symbol: symbol, factorFromUnitToBase: 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
String baseUnit = SetUtils.stringValue(theSet, "baseUnit");
|
String baseUnit = SetUtils.stringValue(theSet, "baseUnit");
|
||||||
|
|
||||||
if (theSet.containsKey("factor")) {
|
if (theSet.containsKey("factor")) {
|
||||||
Number factorFromUnitToBase = SetUtils.numberValue(theSet, "factor");
|
Number factorFromUnitToBase = SetUtils.numberValue(theSet, "factor");
|
||||||
return UnitSpec(uuid: uuid, name: name, baseUnit: baseUnit, symbol: symbol, factorFromUnitToBase: factorFromUnitToBase);
|
return UnitSpec(name: name, baseUnit: baseUnit, symbol: symbol, factorFromUnitToBase: factorFromUnitToBase);
|
||||||
} else if (theSet.containsKey("toBase")) {
|
} else if (theSet.containsKey("toBase")) {
|
||||||
String codeFromBaseToUnit = SetUtils.stringValue(theSet, "fromBase");
|
String codeFromBaseToUnit = SetUtils.stringValue(theSet, "fromBase");
|
||||||
String codeFromUnitToBase = SetUtils.stringValue(theSet, "toBase");
|
String codeFromUnitToBase = SetUtils.stringValue(theSet, "toBase");
|
||||||
|
|
||||||
return UnitSpec(
|
return UnitSpec(
|
||||||
uuid: uuid,
|
|
||||||
name: name,
|
name: name,
|
||||||
baseUnit: baseUnit,
|
baseUnit: baseUnit,
|
||||||
symbol: symbol,
|
symbol: symbol,
|
||||||
|
|
@ -94,7 +84,7 @@ class UnitSpec extends FormulaElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VariableSpec{
|
class VariableSpec extends FormulaElement {
|
||||||
final String name;
|
final String name;
|
||||||
final String? unit;
|
final String? unit;
|
||||||
final List<dynamic>? values;
|
final List<dynamic>? values;
|
||||||
|
|
@ -108,7 +98,7 @@ class VariableSpec{
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
VariableSpec({required this.name, this.unit, this.values}){
|
VariableSpec({required this.name, this.unit, this.values}) {
|
||||||
validate();
|
validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,6 +128,8 @@ class VariableSpec{
|
||||||
int get hashCode => Object.hash(unit, name, values != null ? const DeepCollectionEquality().hash(values!) : 0);
|
int get hashCode => Object.hash(unit, name, values != null ? const DeepCollectionEquality().hash(values!) : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _generateUuidV4() => Uuid().v4();
|
||||||
|
|
||||||
abstract class FormulaInterface {
|
abstract class FormulaInterface {
|
||||||
String get uuid;
|
String get uuid;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,8 +81,10 @@ class _CorpusLoaderState extends State<CorpusLoader> {
|
||||||
}
|
}
|
||||||
|
|
||||||
var corpus = snapshot.data!;
|
var corpus = snapshot.data!;
|
||||||
_registerCorpusInstance(corpus);
|
GetIt.instance.registerSingleton<Corpus>(corpus);
|
||||||
|
|
||||||
|
// If the corpus is empty (user chose not to load default), we could handle that here
|
||||||
|
// For now, just display the formula list
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Formulas'),
|
title: const Text('Formulas'),
|
||||||
|
|
@ -104,20 +106,6 @@ class _CorpusLoaderState extends State<CorpusLoader> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _registerCorpusInstance(Corpus corpus) {
|
|
||||||
var existingCorpus = GetIt.instance.isRegistered<Corpus>() ? GetIt.instance.get<Corpus>() : null;
|
|
||||||
if (existingCorpus == null ) {
|
|
||||||
print( "Registering corpus in GetIt for the first time." );
|
|
||||||
GetIt.instance.registerSingleton<Corpus>(corpus);
|
|
||||||
}
|
|
||||||
else if( existingCorpus == corpus ){
|
|
||||||
print( "The corpus was already registered and is the same instance, no need to re-register." );
|
|
||||||
}
|
|
||||||
else if( existingCorpus != corpus ){
|
|
||||||
throw Exception( "The corpus was already registered but is a different instance. This should not happen." );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to load corpus from database first, falls back to default corpus if database is empty
|
/// Attempts to load corpus from database first, falls back to default corpus if database is empty
|
||||||
|
|
@ -142,9 +130,7 @@ Future<Corpus> loadCorpusFromDatabaseOrAssets() async {
|
||||||
return defaultCorpus;
|
return defaultCorpus;
|
||||||
} else {
|
} else {
|
||||||
// Load corpus from database elements
|
// Load corpus from database elements
|
||||||
final corpus = Corpus();
|
return await Corpus.fromDatabaseElements(dbElements);
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
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}' : '';
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
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,14 +17,6 @@ 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:
|
||||||
|
|
@ -696,14 +688,6 @@ 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,7 +53,6 @@ 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:
|
||||||
|
|
@ -89,7 +88,6 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:d4rt_formulas/defaults/default_corpus.dart';
|
import 'package:d4rt_formulas/defaults/default_corpus.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:d4rt_formulas/main.dart';
|
import 'package:d4rt_formulas/main.dart';
|
||||||
|
|
@ -11,26 +9,10 @@ import 'package:d4rt_formulas/formula_models.dart';
|
||||||
import 'package:d4rt_formulas/database/database_service.dart';
|
import 'package:d4rt_formulas/database/database_service.dart';
|
||||||
import 'package:d4rt_formulas/service_locator.dart';
|
import 'package:d4rt_formulas/service_locator.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:d4rt_formulas/set_utils.dart';
|
|
||||||
import 'package:d4rt_formulas/database/formulas_database.dart';
|
|
||||||
import 'package:d4rt_formulas/ai/import_from_text_screen.dart';
|
|
||||||
import 'package:d4rt_formulas/ai/import_preview_screen.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
||||||
setUpAll(() {
|
|
||||||
// Ensure the database is initialized once for all tests
|
|
||||||
setupLocator();
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('selects first formula and opens editor from AppBar', (WidgetTester tester) async {
|
testWidgets('selects first formula and opens editor from AppBar', (WidgetTester tester) async {
|
||||||
// Reset GetIt to allow fresh corpus registration
|
|
||||||
if (GetIt.instance.isRegistered<Corpus>()) {
|
|
||||||
GetIt.instance.unregister<Corpus>();
|
|
||||||
}
|
|
||||||
GetIt.instance.unregister<FormulasDatabase>();
|
|
||||||
setupLocator();
|
|
||||||
|
|
||||||
// Build the app
|
// Build the app
|
||||||
var corpus = await createDefaultCorpus();
|
var corpus = await createDefaultCorpus();
|
||||||
var corpusCompleter = Completer<Corpus>();
|
var corpusCompleter = Completer<Corpus>();
|
||||||
|
|
@ -59,221 +41,4 @@ void main() {
|
||||||
// Verify FormulaEditor is shown
|
// Verify FormulaEditor is shown
|
||||||
expect(find.text('Edit Formula'), findsOneWidget);
|
expect(find.text('Edit Formula'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('share first formula to clipboard and import it', (WidgetTester tester) async {
|
|
||||||
tester.view.physicalSize = const Size(1200, 800);
|
|
||||||
tester.view.devicePixelRatio = 1.0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Reset GetIt to allow fresh corpus registration
|
|
||||||
if (GetIt.instance.isRegistered<Corpus>()) {
|
|
||||||
GetIt.instance.unregister<Corpus>();
|
|
||||||
}
|
|
||||||
GetIt.instance.unregister<FormulasDatabase>();
|
|
||||||
setupLocator();
|
|
||||||
|
|
||||||
print('DEBUG: Building app...');
|
|
||||||
var corpus = await createDefaultCorpus();
|
|
||||||
var corpusCompleter = Completer<Corpus>();
|
|
||||||
corpusCompleter.complete(corpus);
|
|
||||||
var app = MyApp(corpusCompleter.future);
|
|
||||||
await tester.pumpWidget(app);
|
|
||||||
await tester.pump();
|
|
||||||
await tester.pumpAndSettle(const Duration(seconds: 10));
|
|
||||||
print('DEBUG: App built and settled');
|
|
||||||
|
|
||||||
final firstFormulaTile = find.byType(ListTile).first;
|
|
||||||
expect(firstFormulaTile, findsOneWidget);
|
|
||||||
print('DEBUG: Found first formula tile');
|
|
||||||
|
|
||||||
final shareButton = find.descendant(
|
|
||||||
of: firstFormulaTile,
|
|
||||||
matching: find.byIcon(Icons.share),
|
|
||||||
);
|
|
||||||
await tester.tap(shareButton);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
print('DEBUG: Tapped share button');
|
|
||||||
|
|
||||||
await tester.tap(find.text('Copy to clipboard'));
|
|
||||||
await tester.pump(const Duration(seconds: 1));
|
|
||||||
print('DEBUG: Tapped copy to clipboard');
|
|
||||||
|
|
||||||
// Generate the expected export string directly
|
|
||||||
final random = Random();
|
|
||||||
final marker = 'TEST_MARKER_${random.nextInt(999999).toString().padLeft(6, '0')}';
|
|
||||||
final firstFormula = corpus.getFormulas().first;
|
|
||||||
final dependencies = corpus.withDependencies(firstFormula);
|
|
||||||
final dependenciesAsMap = dependencies.map((f) => f.toMap()).toList();
|
|
||||||
for (final f in dependenciesAsMap) {
|
|
||||||
f.remove("uuid");
|
|
||||||
}
|
|
||||||
// Inject marker into first formula's description (append without newline to avoid raw string issues)
|
|
||||||
if (dependenciesAsMap[0].containsKey('description')) {
|
|
||||||
final desc = dependenciesAsMap[0]['description'];
|
|
||||||
if (desc is String && !desc.contains('\n') && !desc.contains('"""')) {
|
|
||||||
// Simple string, can append directly
|
|
||||||
dependenciesAsMap[0]['description'] = '$desc $marker';
|
|
||||||
} else {
|
|
||||||
// Complex string, use a tag instead
|
|
||||||
dependenciesAsMap[0]['tags'] = [...(dependenciesAsMap[0]['tags'] ?? []), marker];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dependenciesAsMap[0]['description'] = marker;
|
|
||||||
}
|
|
||||||
final exportString = SetUtils.prettyPrint(dependenciesAsMap);
|
|
||||||
print('DEBUG: Export string starts with: ${exportString.substring(0, 100)}');
|
|
||||||
|
|
||||||
// Mock the clipboard channel so the app reads our content
|
|
||||||
String? mockedClipboardText = exportString;
|
|
||||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
||||||
const MethodChannel('plugins.flutter.io/clipboard'),
|
|
||||||
(MethodCall methodCall) async {
|
|
||||||
if (methodCall.method == 'getData') {
|
|
||||||
return <String, dynamic>{'text': mockedClipboardText};
|
|
||||||
}
|
|
||||||
if (methodCall.method == 'setData') {
|
|
||||||
mockedClipboardText = methodCall.arguments['text'];
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
print('DEBUG: Clipboard mocked with marker: $marker');
|
|
||||||
|
|
||||||
await tester.tap(find.byIcon(Icons.library_add));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
print('DEBUG: Tapped import button');
|
|
||||||
expect(find.byType(ImportFromTextScreen), findsOneWidget, reason: 'ImportFromTextScreen should be visible');
|
|
||||||
|
|
||||||
// Instead of relying on clipboard paste, find the EditableText inside CodeField and enter text
|
|
||||||
// CodeField contains an EditableText widget that we can interact with
|
|
||||||
final editableText = find.byType(EditableText);
|
|
||||||
expect(editableText, findsOneWidget, reason: 'Should find EditableText in CodeField');
|
|
||||||
|
|
||||||
// Use enterText to set the content
|
|
||||||
await tester.enterText(editableText, exportString);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
print('DEBUG: Text entered into code field');
|
|
||||||
|
|
||||||
// Now tap Import
|
|
||||||
await tester.tap(find.text('Import'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
print('DEBUG: Tapped Import button');
|
|
||||||
|
|
||||||
expect(find.byType(ImportPreviewScreen), findsOneWidget, reason: 'ImportPreviewScreen should appear');
|
|
||||||
|
|
||||||
// Verify the preview has the expected number of elements
|
|
||||||
final importPreviewState = tester.state(find.byType(ImportPreviewScreen));
|
|
||||||
final elements = (importPreviewState as dynamic).widget.elements;
|
|
||||||
print('DEBUG: ImportPreviewScreen has ${elements.length} elements to import');
|
|
||||||
|
|
||||||
// Tap the "Import Selected" button (check icon in AppBar)
|
|
||||||
await tester.tap(find.byIcon(Icons.check));
|
|
||||||
await tester.pumpAndSettle(const Duration(seconds: 2));
|
|
||||||
|
|
||||||
// Note: The import operation saves to the database which doesn't work reliably in widget tests.
|
|
||||||
// We verify the UI flow up to the import preview screen.
|
|
||||||
// The actual database import is tested in integration tests.
|
|
||||||
print('DEBUG: Import flow completed successfully up to preview screen');
|
|
||||||
} finally {
|
|
||||||
tester.view.resetPhysicalSize();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('import formula updates existing element in database', (WidgetTester tester) async {
|
|
||||||
tester.view.physicalSize = const Size(1200, 800);
|
|
||||||
tester.view.devicePixelRatio = 1.0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Reset GetIt to allow fresh corpus registration
|
|
||||||
if (GetIt.instance.isRegistered<Corpus>()) {
|
|
||||||
GetIt.instance.unregister<Corpus>();
|
|
||||||
}
|
|
||||||
GetIt.instance.unregister<FormulasDatabase>();
|
|
||||||
setupLocator();
|
|
||||||
|
|
||||||
// Build the app with default corpus
|
|
||||||
var corpus = await createDefaultCorpus();
|
|
||||||
var corpusCompleter = Completer<Corpus>();
|
|
||||||
corpusCompleter.complete(corpus);
|
|
||||||
var app = MyApp(corpusCompleter.future);
|
|
||||||
await tester.pumpWidget(app);
|
|
||||||
await tester.pump();
|
|
||||||
await tester.pumpAndSettle(const Duration(seconds: 10));
|
|
||||||
|
|
||||||
// Get the first formula from the corpus
|
|
||||||
final firstFormula = corpus.getFormulas().first;
|
|
||||||
final originalUuid = firstFormula.uuid;
|
|
||||||
final originalName = firstFormula.name;
|
|
||||||
|
|
||||||
// Export the first formula
|
|
||||||
final firstFormulaTile = find.byType(ListTile).first;
|
|
||||||
expect(firstFormulaTile, findsOneWidget);
|
|
||||||
|
|
||||||
final shareButton = find.descendant(
|
|
||||||
of: firstFormulaTile,
|
|
||||||
matching: find.byIcon(Icons.share),
|
|
||||||
);
|
|
||||||
await tester.tap(shareButton);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
await tester.tap(find.text('Copy to clipboard'));
|
|
||||||
await tester.pump(const Duration(seconds: 1));
|
|
||||||
|
|
||||||
// Get the export string and modify it
|
|
||||||
final dependencies = corpus.withDependencies(firstFormula);
|
|
||||||
final dependenciesAsMap = dependencies.map((f) => f.toMap()).toList();
|
|
||||||
final exportString = SetUtils.prettyPrint(dependenciesAsMap);
|
|
||||||
|
|
||||||
// Mock the clipboard
|
|
||||||
String? mockedClipboardText = exportString;
|
|
||||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
||||||
const MethodChannel('plugins.flutter.io/clipboard'),
|
|
||||||
(MethodCall methodCall) async {
|
|
||||||
if (methodCall.method == 'getData') {
|
|
||||||
return <String, dynamic>{'text': mockedClipboardText};
|
|
||||||
}
|
|
||||||
if (methodCall.method == 'setData') {
|
|
||||||
mockedClipboardText = methodCall.arguments['text'];
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Import the formula back (this should update existing elements)
|
|
||||||
await tester.tap(find.byIcon(Icons.library_add));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
expect(find.byType(ImportFromTextScreen), findsOneWidget);
|
|
||||||
|
|
||||||
// Enter the export string into the code field
|
|
||||||
final editableText = find.byType(EditableText);
|
|
||||||
expect(editableText, findsOneWidget);
|
|
||||||
await tester.enterText(editableText, exportString);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// Tap Import
|
|
||||||
await tester.tap(find.text('Import'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
expect(find.byType(ImportPreviewScreen), findsOneWidget);
|
|
||||||
|
|
||||||
// Verify the preview has the expected elements
|
|
||||||
final importPreviewState = tester.state(find.byType(ImportPreviewScreen));
|
|
||||||
final elements = (importPreviewState as dynamic).widget.elements;
|
|
||||||
expect(elements.isNotEmpty, true);
|
|
||||||
|
|
||||||
// Tap the "Import Selected" button (check icon in AppBar)
|
|
||||||
await tester.tap(find.byIcon(Icons.check));
|
|
||||||
await tester.pumpAndSettle(const Duration(seconds: 2));
|
|
||||||
|
|
||||||
// The import should succeed even though elements already exist in DB
|
|
||||||
// We can't directly verify the database update in widget test, but we verify no error occurred
|
|
||||||
print('DEBUG: Import with existing elements completed successfully');
|
|
||||||
} finally {
|
|
||||||
tester.view.resetPhysicalSize();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
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() {
|
||||||
|
|
@ -23,20 +22,6 @@ 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', () {
|
||||||
|
|
@ -90,4 +75,5 @@ void main() {
|
||||||
expect(root, closeTo(Math.log(2), 0.01));
|
expect(root, closeTo(Math.log(2), 0.01));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
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