Merge branch 'feature/import-formulas'
This commit is contained in:
commit
a7178e2e81
16 changed files with 667 additions and 257 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -18,3 +18,6 @@
|
||||||
.aider*
|
.aider*
|
||||||
.build-container-cache
|
.build-container-cache
|
||||||
/coverage/
|
/coverage/
|
||||||
|
/.agent-shell/
|
||||||
|
/ios/
|
||||||
|
/macos/
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
- `flutter pub get` --> `./flutterw pub get`
|
- `flutter pub get` --> `./flutterw pub get`
|
||||||
- `flutter run -d linux` --> `./flutterw run -d linux`
|
- `flutter run -d linux` --> `./flutterw run -d linux`
|
||||||
- See `./Makefile` for more examples.
|
- See `./Makefile` for more examples.
|
||||||
|
- If you are an agent, you may be also containerized. Try `distrobox-host-exec $(pwd)/flutterw`
|
||||||
|
|
||||||
|
|
||||||
# MANDATORY WORKFLOW
|
# MANDATORY WORKFLOW
|
||||||
|
|
|
||||||
26
Makefile
26
Makefile
|
|
@ -1,43 +1,45 @@
|
||||||
|
|
||||||
all: build-container clean-container build-builders build-linux-debug-container
|
all: build-container clean-container build-builders build-linux-debug-container
|
||||||
|
|
||||||
DB=~/.local/share/com.example.d4rt_formulas/d4rt_formulas/formulas.sqlite
|
DATABASEFILE=~/.local/share/com.example.d4rt_formulas/d4rt_formulas/formulas.sqlite
|
||||||
|
|
||||||
|
FLUTTERW := $(shell if [ "$$CONTAINER_ID" = "" ]; then echo "./flutterw"; else echo "distrobox-host-exec $(CURDIR)/flutterw"; fi)
|
||||||
|
|
||||||
build-container:
|
build-container:
|
||||||
./flutterw --build-container
|
$(FLUTTERW) --build-container
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
flutter clean
|
flutter clean
|
||||||
[ -f $(DB) ] && rm $(DB)
|
[ -f $(DATABASEFILE) ] && rm $(DATABASEFILE)
|
||||||
|
|
||||||
clean-container:
|
clean-container:
|
||||||
rm -r .build-container-cache
|
rm -r .build-container-cache
|
||||||
./flutterw clean
|
$(FLUTTERW) clean
|
||||||
|
|
||||||
|
|
||||||
pub-get-container:
|
pub-get-container:
|
||||||
./flutterw pub get
|
$(FLUTTERW) pub get
|
||||||
|
|
||||||
test:
|
test:
|
||||||
./flutterw test
|
$(FLUTTERW) test
|
||||||
|
|
||||||
build-builders:
|
build-builders:
|
||||||
./flutterw pub run build_runner build --delete-conflicting-outputs
|
$(FLUTTERW) pub run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
build-android-release-container:
|
build-android-release-container:
|
||||||
./flutterw build apk --release
|
$(FLUTTERW) build apk --release
|
||||||
|
|
||||||
build-linux-debug-container:
|
build-linux-debug-container:
|
||||||
./flutterw build linux --debug
|
$(FLUTTERW) build linux --debug
|
||||||
|
|
||||||
build-web-debug-container:
|
build-web-debug-container:
|
||||||
./flutterw build web --debug
|
$(FLUTTERW) build web --debug
|
||||||
|
|
||||||
run-linux-debug-container:
|
run-linux-debug-container:
|
||||||
./flutterw run -d linux
|
$(FLUTTERW) run -d linux
|
||||||
|
|
||||||
run-web-debug-container:
|
run-web-debug-container:
|
||||||
./flutterw run --web-port $${WEB_PORT:-8081} -d web-server
|
$(FLUTTERW) run --web-port $${WEB_PORT:-8081} -d web-server
|
||||||
|
|
||||||
run-linux-debug-native:
|
run-linux-debug-native:
|
||||||
flutter run -d linux
|
flutter run -d linux
|
||||||
|
|
|
||||||
11
TODO.md
11
TODO.md
|
|
@ -67,17 +67,18 @@
|
||||||
- [R] When a formula is derived in FormulaScreen, the new FormulaScreen is not pushed in navigator, it replacles the current FormulaScreen
|
- [R] When a formula is derived in FormulaScreen, the new FormulaScreen is not pushed in navigator, it replacles the current FormulaScreen
|
||||||
- [R] In FormulaScreen, a Formula cant be derived if DerivedFormula.isDerivable() returns false
|
- [R] In FormulaScreen, a Formula cant be derived if DerivedFormula.isDerivable() returns false
|
||||||
- [R] The algorithm of formulaSolver should be https://en.wikipedia.org/wiki/Newton%27s_method
|
- [R] The algorithm of formulaSolver should be https://en.wikipedia.org/wiki/Newton%27s_method
|
||||||
- [ ] Use receive_sharing_intent package to implement import of files in linux and android.
|
- [R] Use receive_sharing_intent package to implement import of files in linux and android.
|
||||||
- The application will accept *.d4rtf files with the same format of files in ./assets .
|
- The application will accept *.d4rtf files with the same format of files in ./assets .
|
||||||
- The application will accept also shared text, with same format as files in ./assets.
|
- The application will accept also shared text, with same format as files in ./assets.
|
||||||
- The loaded formulaelemets will be added to the GetIt.instance.get<Corpus)()
|
- The loaded formulaelemets will be added to the GetIt.instance.get<Corpus>()
|
||||||
- [ ] Preview of imported formulalements
|
- [R] Preview of imported formulalements
|
||||||
- The screen will receive a list of FormulaElements to import
|
- The screen will receive a list of FormulaElements to import
|
||||||
- The formulas will have a "edit" button to show a FormulaEditor with the formula
|
- The formulas will have a "edit" button to show a FormulaEditor with the formula
|
||||||
- The screen will have an "import all" button to import all the FormulaElements in the list. This will call Corpus.addFormulaElement() for each element, and then pop the screen.
|
- The screen will have an "import all" button to import all the FormulaElements in the list. This will call Corpus.addFormulaElement() for each element, and then pop the screen.
|
||||||
- [ ] In FormulaList, add a button next to "export" to import FormulaElements.
|
- [R] In FormulaList, add a button next to "export" to import FormulaElements.
|
||||||
- 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
|
||||||
- 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.
|
|
||||||
- [ ] 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.
|
- [ ] 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.
|
||||||
|
- [ ] 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.
|
||||||
|
- [ ] When importing FormulaElements, save the FormulaElements in the database (currently, they are only added to the Corpus in memory).
|
||||||
|
|
|
||||||
29
android/d4rt_formulas_android.iml
Normal file
29
android/d4rt_formulas_android.iml
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="FacetManager">
|
||||||
|
<facet type="android" name="Android">
|
||||||
|
<configuration>
|
||||||
|
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||||
|
<option name="GEN_FOLDER_RELATIVE_PATH_APT" value="/gen" />
|
||||||
|
<option name="GEN_FOLDER_RELATIVE_PATH_AIDL" value="/gen" />
|
||||||
|
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/app/src/main/AndroidManifest.xml" />
|
||||||
|
<option name="RES_FOLDER_RELATIVE_PATH" value="/app/src/main/res" />
|
||||||
|
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/app/src/main/assets" />
|
||||||
|
<option name="LIBS_FOLDER_RELATIVE_PATH" value="/app/src/main/libs" />
|
||||||
|
<option name="PROGUARD_LOGS_FOLDER_RELATIVE_PATH" value="/app/src/main/proguard_logs" />
|
||||||
|
</configuration>
|
||||||
|
</facet>
|
||||||
|
</component>
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/app/src/main/java" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/app/src/main/kotlin" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Android API 24 Platform" jdkType="Android SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
<orderEntry type="library" name="Flutter for Android" level="project" />
|
||||||
|
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
|
|
@ -9,6 +9,9 @@ import '../database/database_service.dart';
|
||||||
import '../service_locator.dart';
|
import '../service_locator.dart';
|
||||||
import 'formula_screen.dart';
|
import 'formula_screen.dart';
|
||||||
import 'unit_dropdown.dart';
|
import 'unit_dropdown.dart';
|
||||||
|
import 'package:flutter_code_editor/flutter_code_editor.dart';
|
||||||
|
import 'package:flutter_highlight/themes/monokai-sublime.dart';
|
||||||
|
import 'package:highlight/languages/dart.dart';
|
||||||
|
|
||||||
/// A screen for editing a Formula's properties including name, description,
|
/// A screen for editing a Formula's properties including name, description,
|
||||||
/// input/output variables, and d4rt code.
|
/// input/output variables, and d4rt code.
|
||||||
|
|
@ -17,12 +20,7 @@ class FormulaEditor extends StatefulWidget {
|
||||||
final Corpus corpus;
|
final Corpus corpus;
|
||||||
final Function(Formula)? onSave;
|
final Function(Formula)? onSave;
|
||||||
|
|
||||||
const FormulaEditor({
|
const FormulaEditor({super.key, required this.formula, required this.corpus, this.onSave});
|
||||||
super.key,
|
|
||||||
required this.formula,
|
|
||||||
required this.corpus,
|
|
||||||
this.onSave,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FormulaEditor> createState() => _FormulaEditorState();
|
State<FormulaEditor> createState() => _FormulaEditorState();
|
||||||
|
|
@ -32,7 +30,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
late TextEditingController _nameController;
|
late TextEditingController _nameController;
|
||||||
late TextEditingController _descriptionController;
|
late TextEditingController _descriptionController;
|
||||||
late TextEditingController _d4rtCodeController;
|
late CodeController _d4rtCodeController;
|
||||||
|
|
||||||
// Track input variables
|
// Track input variables
|
||||||
final List<_InputVariableRowData> _inputVariables = [];
|
final List<_InputVariableRowData> _inputVariables = [];
|
||||||
|
|
@ -47,15 +45,17 @@ class _FormulaEditorState extends State<FormulaEditor> {
|
||||||
super.initState();
|
super.initState();
|
||||||
_nameController = TextEditingController(text: widget.formula.name);
|
_nameController = TextEditingController(text: widget.formula.name);
|
||||||
_descriptionController = TextEditingController(text: widget.formula.description ?? '');
|
_descriptionController = TextEditingController(text: widget.formula.description ?? '');
|
||||||
_d4rtCodeController = TextEditingController(text: widget.formula.d4rtCode);
|
_d4rtCodeController = CodeController(language: dart, text: widget.formula.d4rtCode ?? '');
|
||||||
|
|
||||||
// Initialize input variables
|
// Initialize input variables
|
||||||
for (final input in widget.formula.input) {
|
for (final input in widget.formula.input) {
|
||||||
_inputVariables.add(_InputVariableRowData(
|
_inputVariables.add(
|
||||||
nameController: TextEditingController(text: input.name),
|
_InputVariableRowData(
|
||||||
unit: input.unit,
|
nameController: TextEditingController(text: input.name),
|
||||||
values: input.values != null ? List.from(input.values!) : null,
|
unit: input.unit,
|
||||||
));
|
values: input.values != null ? List.from(input.values!) : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize output variable
|
// Initialize output variable
|
||||||
|
|
@ -79,11 +79,13 @@ class _FormulaEditorState extends State<FormulaEditor> {
|
||||||
|
|
||||||
void _addInputVariable() {
|
void _addInputVariable() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_inputVariables.add(_InputVariableRowData(
|
_inputVariables.add(
|
||||||
nameController: TextEditingController(text: 'var${_inputVariables.length + 1}'),
|
_InputVariableRowData(
|
||||||
unit: null,
|
nameController: TextEditingController(text: 'var${_inputVariables.length + 1}'),
|
||||||
values: null,
|
unit: null,
|
||||||
));
|
values: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,10 +119,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => FormulaScreen(
|
builder: (context) => FormulaScreen(formula: formula, corpus: widget.corpus),
|
||||||
formula: formula,
|
|
||||||
corpus: widget.corpus,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -159,17 +158,12 @@ class _FormulaEditorState extends State<FormulaEditor> {
|
||||||
try {
|
try {
|
||||||
final input = <VariableSpec>[];
|
final input = <VariableSpec>[];
|
||||||
for (final variable in _inputVariables) {
|
for (final variable in _inputVariables) {
|
||||||
input.add(VariableSpec(
|
input.add(
|
||||||
name: variable.nameController.text.trim(),
|
VariableSpec(name: variable.nameController.text.trim(), unit: variable.unit, values: variable.values),
|
||||||
unit: variable.unit,
|
);
|
||||||
values: variable.values,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final output = VariableSpec(
|
final output = VariableSpec(name: _outputVariable.nameController.text.trim(), unit: _outputVariable.unit);
|
||||||
name: _outputVariable.nameController.text.trim(),
|
|
||||||
unit: _outputVariable.unit,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Formula(
|
return Formula(
|
||||||
uuid: widget.formula.uuid,
|
uuid: widget.formula.uuid,
|
||||||
|
|
@ -177,7 +171,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
|
||||||
description: _descriptionController.text.isEmpty ? null : _descriptionController.text,
|
description: _descriptionController.text.isEmpty ? null : _descriptionController.text,
|
||||||
input: input,
|
input: input,
|
||||||
output: output,
|
output: output,
|
||||||
d4rtCode: _d4rtCodeController.text,
|
d4rtCode: _d4rtCodeController.fullText,
|
||||||
tags: widget.formula.tags, // Preserve existing tags
|
tags: widget.formula.tags, // Preserve existing tags
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -296,21 +290,9 @@ class _FormulaEditorState extends State<FormulaEditor> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Edit Formula'),
|
title: const Text('Edit Formula'),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(icon: const Icon(Icons.play_arrow), onPressed: _testFormula, tooltip: 'Test Formula'),
|
||||||
icon: const Icon(Icons.play_arrow),
|
IconButton(icon: const Icon(Icons.copy), onPressed: _saveFormulaAsCopy, tooltip: 'Save as copy'),
|
||||||
onPressed: _testFormula,
|
IconButton(icon: const Icon(Icons.save), onPressed: _saveFormula, tooltip: 'Save'),
|
||||||
tooltip: 'Test Formula',
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.copy),
|
|
||||||
onPressed: _saveFormulaAsCopy,
|
|
||||||
tooltip: 'Save as copy',
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.save),
|
|
||||||
onPressed: _saveFormula,
|
|
||||||
tooltip: 'Save',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Form(
|
body: Form(
|
||||||
|
|
@ -363,13 +345,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text('Description (Markdown)', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||||
'Description (Markdown)',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -400,13 +376,8 @@ class _FormulaEditorState extends State<FormulaEditor> {
|
||||||
child: Markdown(
|
child: Markdown(
|
||||||
data: _descriptionController.text,
|
data: _descriptionController.text,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
builders: {
|
builders: {'latex': LatexElementBuilder()},
|
||||||
'latex': LatexElementBuilder(),
|
extensionSet: markdown.ExtensionSet([LatexBlockSyntax()], [LatexInlineSyntax()]),
|
||||||
},
|
|
||||||
extensionSet: markdown.ExtensionSet(
|
|
||||||
[LatexBlockSyntax()],
|
|
||||||
[LatexInlineSyntax()],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
@ -432,13 +403,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text('Input Variables', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||||
'Input Variables',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
..._inputVariables.asMap().entries.map((entry) {
|
..._inputVariables.asMap().entries.map((entry) {
|
||||||
final index = entry.key;
|
final index = entry.key;
|
||||||
|
|
@ -470,10 +435,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: variable.nameController,
|
controller: variable.nameController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(labelText: 'Name', border: OutlineInputBorder()),
|
||||||
labelText: 'Name',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
|
@ -530,7 +492,8 @@ class _FormulaEditorState extends State<FormulaEditor> {
|
||||||
final unitSpec = widget.corpus.getUnit(unit);
|
final unitSpec = widget.corpus.getUnit(unit);
|
||||||
return DropdownMenuItem<String?>(
|
return DropdownMenuItem<String?>(
|
||||||
value: unit,
|
value: unit,
|
||||||
child: Text('${unitSpec.symbol} - ${unit}',
|
child: Text(
|
||||||
|
'${unitSpec.symbol} - ${unit}',
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
|
@ -567,13 +530,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text('Output Variable', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||||
'Output Variable',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -581,10 +538,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _outputVariable.nameController,
|
controller: _outputVariable.nameController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(labelText: 'Name', border: OutlineInputBorder()),
|
||||||
labelText: 'Name',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
|
@ -641,7 +595,8 @@ class _FormulaEditorState extends State<FormulaEditor> {
|
||||||
final unitSpec = widget.corpus.getUnit(unit);
|
final unitSpec = widget.corpus.getUnit(unit);
|
||||||
return DropdownMenuItem<String?>(
|
return DropdownMenuItem<String?>(
|
||||||
value: unit,
|
value: unit,
|
||||||
child: Text('${unitSpec.symbol} - ${unit}',
|
child: Text(
|
||||||
|
'${unitSpec.symbol} - ${unit}',
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
|
@ -667,67 +622,18 @@ class _FormulaEditorState extends State<FormulaEditor> {
|
||||||
|
|
||||||
Widget _buildD4rtCodeSection() {
|
Widget _buildD4rtCodeSection() {
|
||||||
return Card(
|
return Card(
|
||||||
child: Padding(
|
child: Expanded(
|
||||||
padding: const EdgeInsets.all(16.0),
|
child: Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.all(16.0),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: CodeTheme(
|
||||||
children: [
|
data: CodeThemeData(styles: monokaiSublimeTheme),
|
||||||
const Text(
|
child: Padding(
|
||||||
'D4RT Code',
|
padding: const EdgeInsets.all(8.0),
|
||||||
style: TextStyle(
|
child: SingleChildScrollView(
|
||||||
fontSize: 16,
|
child: CodeField(controller: _d4rtCodeController),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: Theme.of(context).dividerColor),
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
|
||||||
borderRadius: const BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(4),
|
|
||||||
topRight: Radius.circular(4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.code, size: 16, color: Theme.of(context).colorScheme.primary),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'Dart Syntax',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
controller: _d4rtCodeController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
hintText: 'Enter D4RT/Dart code here',
|
|
||||||
border: InputBorder.none,
|
|
||||||
contentPadding: EdgeInsets.all(12),
|
|
||||||
),
|
|
||||||
maxLines: 10,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -763,19 +669,12 @@ class _InputVariableRowData {
|
||||||
String? unit;
|
String? unit;
|
||||||
List<dynamic>? values;
|
List<dynamic>? values;
|
||||||
|
|
||||||
_InputVariableRowData({
|
_InputVariableRowData({required this.nameController, this.unit, this.values});
|
||||||
required this.nameController,
|
|
||||||
this.unit,
|
|
||||||
this.values,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _OutputVariableRowData {
|
class _OutputVariableRowData {
|
||||||
final TextEditingController nameController;
|
final TextEditingController nameController;
|
||||||
String? unit;
|
String? unit;
|
||||||
|
|
||||||
_OutputVariableRowData({
|
_OutputVariableRowData({required this.nameController, this.unit});
|
||||||
required this.nameController,
|
|
||||||
this.unit,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,17 @@ import 'formula_screen.dart';
|
||||||
import 'package:share_plus/share_plus.dart' as share_plus;
|
import 'package:share_plus/share_plus.dart' as share_plus;
|
||||||
import 'formula_editor.dart';
|
import 'formula_editor.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'import_preview_screen.dart';
|
||||||
|
import '../services/import_service.dart';
|
||||||
|
|
||||||
class FormulaList extends StatefulWidget {
|
class FormulaList extends StatefulWidget {
|
||||||
final Corpus corpus;
|
final Corpus corpus;
|
||||||
|
final VoidCallback? onImport;
|
||||||
|
|
||||||
const FormulaList({
|
const FormulaList({
|
||||||
super.key,
|
super.key,
|
||||||
required this.corpus,
|
required this.corpus,
|
||||||
|
this.onImport,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -77,23 +81,6 @@ class _FormulaListState extends State<FormulaList> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _editFormula(Formula formula) {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => FormulaEditor(
|
|
||||||
formula: formula,
|
|
||||||
corpus: widget.corpus,
|
|
||||||
onSave: (updatedFormula){
|
|
||||||
setState((){
|
|
||||||
// THIS UPDATES THE FORMULA LIST
|
|
||||||
});
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _copyFormula(Formula formula) async {
|
void _copyFormula(Formula formula) async {
|
||||||
try {
|
try {
|
||||||
final exportString = _formulaAndDependenciesToExportStringLiteral(formula);
|
final exportString = _formulaAndDependenciesToExportStringLiteral(formula);
|
||||||
|
|
@ -133,6 +120,7 @@ class _FormulaListState extends State<FormulaList> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
|
|
@ -162,13 +150,9 @@ class _FormulaListState extends State<FormulaList> {
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.edit),
|
|
||||||
onPressed: () => _editFormula(formula),
|
|
||||||
tooltip: 'Edit Formula',
|
|
||||||
),
|
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
icon: const Icon(Icons.share),
|
icon: const Icon(Icons.share),
|
||||||
|
tooltip: 'Share or copy to clipboard',
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
if (value == 'share') {
|
if (value == 'share') {
|
||||||
_shareFormula(formula);
|
_shareFormula(formula);
|
||||||
|
|
|
||||||
|
|
@ -121,14 +121,13 @@ class _FormulaScreenState extends State<FormulaScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
late final dynamic result;
|
late final dynamic result;
|
||||||
//if( formula is DerivedFormula) {
|
if( formula is DerivedFormula) {
|
||||||
result = formulaSolver(formula, formula.output.name, inputValues,);
|
result = formulaSolver(formula, formula.output.name, inputValues,);
|
||||||
//}
|
}
|
||||||
//else {
|
else {
|
||||||
// TODO: MAYBE ONLY FORMULASOLVER IS NECCESSARY"
|
final evaluator = FormulaEvaluator();
|
||||||
//final evaluator = FormulaEvaluator();
|
result = evaluator.evaluate(formula as Formula, inputValues);
|
||||||
//result = evaluator.evaluate(formula as Formula, inputValues);
|
}
|
||||||
//}
|
|
||||||
|
|
||||||
// Convert output to selected unit if needed
|
// Convert output to selected unit if needed
|
||||||
String? unit = formula.output.unit;
|
String? unit = formula.output.unit;
|
||||||
|
|
@ -342,7 +341,7 @@ class _FormulaScreenState extends State<FormulaScreen> {
|
||||||
children: [
|
children: [
|
||||||
// Fixed width for field name
|
// Fixed width for field name
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 150,
|
width: 50,
|
||||||
child: Text(
|
child: Text(
|
||||||
formula.output.name,
|
formula.output.name,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
|
@ -353,7 +352,7 @@ class _FormulaScreenState extends State<FormulaScreen> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
enabled: false,
|
enabled: true,
|
||||||
controller: TextEditingController(text: _result),
|
controller: TextEditingController(text: _result),
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
border: UnderlineInputBorder(),
|
border: UnderlineInputBorder(),
|
||||||
|
|
@ -388,10 +387,10 @@ class _FormulaScreenState extends State<FormulaScreen> {
|
||||||
children: [
|
children: [
|
||||||
// Fixed width for field name
|
// Fixed width for field name
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 150,
|
width: 50,
|
||||||
child: Text(
|
child: Text(
|
||||||
variable.name,
|
variable.name,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.fade
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8), // Add some spacing
|
const SizedBox(width: 8), // Add some spacing
|
||||||
|
|
|
||||||
140
lib/ai/import_from_text_screen.dart
Normal file
140
lib/ai/import_from_text_screen.dart
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
import 'package:d4rt_formulas/d4rt_formulas.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_code_editor/flutter_code_editor.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:flutter_highlight/themes/monokai-sublime.dart';
|
||||||
|
import 'package:highlight/languages/dart.dart';
|
||||||
|
|
||||||
|
import '../database/database_service.dart';
|
||||||
|
import '../service_locator.dart';
|
||||||
|
|
||||||
|
import '../services/import_service.dart';
|
||||||
|
import 'formula_list.dart';
|
||||||
|
import '../corpus.dart';
|
||||||
|
import '../defaults/default_corpus.dart';
|
||||||
|
import '../formula_models.dart' as models;
|
||||||
|
import 'import_preview_screen.dart';
|
||||||
|
|
||||||
|
|
||||||
|
/// Screen to import formula elements from text
|
||||||
|
class ImportFromTextScreen extends StatefulWidget {
|
||||||
|
final Corpus corpus;
|
||||||
|
|
||||||
|
const ImportFromTextScreen({super.key, required this.corpus});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImportFromTextScreen> createState() => _ImportFromTextScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImportFromTextScreenState extends State<ImportFromTextScreen> {
|
||||||
|
final CodeController _codeController = CodeController(language: dart, text: "// Insert code here...");
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_codeController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pasteFromClipboard() async {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final clipboardData = await Clipboard.getData('text/plain');
|
||||||
|
if (clipboardData?.text != null) {
|
||||||
|
_codeController.text = clipboardData!.text!;
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(const SnackBar(content: Text('Clipboard is empty'), backgroundColor: Colors.orange));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text('Error pasting from clipboard: $e'), backgroundColor: Colors.red));
|
||||||
|
} finally {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _import() async {
|
||||||
|
final text = _codeController.fullText;
|
||||||
|
if (text.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Please enter or paste formula text'), backgroundColor: Colors.orange),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final importService = ImportService();
|
||||||
|
final elements = importService.parseSharedText(text);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
final result = await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ImportPreviewScreen(elements: elements, corpus: widget.corpus),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == true) {
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text('Error parsing text: $e'), backgroundColor: Colors.red));
|
||||||
|
} finally {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Import from Text')),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: CodeTheme(
|
||||||
|
data: CodeThemeData(styles: monokaiSublimeTheme),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: SingleChildScrollView(child: CodeField(controller: _codeController)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: _isLoading ? null : _pasteFromClipboard,
|
||||||
|
icon: _isLoading
|
||||||
|
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
|
||||||
|
: const Icon(Icons.content_paste),
|
||||||
|
label: const Text('Paste'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: _isLoading ? null : _import,
|
||||||
|
icon: const Icon(Icons.library_add),
|
||||||
|
label: const Text('Import'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
212
lib/ai/import_preview_screen.dart
Normal file
212
lib/ai/import_preview_screen.dart
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:d4rt_formulas/formula_models.dart';
|
||||||
|
import 'package:d4rt_formulas/corpus.dart';
|
||||||
|
import 'package:d4rt_formulas/ai/formula_editor.dart';
|
||||||
|
import 'package:d4rt_formulas/services/import_service.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_code_editor/flutter_code_editor.dart';
|
||||||
|
import 'package:flutter_highlight/themes/monokai-sublime.dart';
|
||||||
|
import 'package:highlight/languages/dart.dart';
|
||||||
|
|
||||||
|
/// Screen to preview and import formula elements
|
||||||
|
class ImportPreviewScreen extends StatefulWidget {
|
||||||
|
final List<FormulaElement> elements;
|
||||||
|
final Corpus corpus;
|
||||||
|
|
||||||
|
const ImportPreviewScreen({super.key, required this.elements, required this.corpus});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImportPreviewScreen> createState() => _ImportPreviewScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImportPreviewScreenState extends State<ImportPreviewScreen> {
|
||||||
|
final Set<String> _selectedUuids = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Select all by default
|
||||||
|
for (final element in widget.elements) {
|
||||||
|
if (element is Formula) {
|
||||||
|
_selectedUuids.add(element.uuid);
|
||||||
|
} else if (element is UnitSpec) {
|
||||||
|
_selectedUuids.add(element.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _editFormulaElement(FormulaElement element) {
|
||||||
|
if (element is Formula) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => FormulaEditor(
|
||||||
|
formula: element,
|
||||||
|
corpus: widget.corpus,
|
||||||
|
onSave: (updatedFormula) {
|
||||||
|
// Update the element in the list
|
||||||
|
setState(() {
|
||||||
|
final index = widget.elements.indexWhere((e) => e is Formula && e.uuid == updatedFormula.uuid);
|
||||||
|
if (index != -1) {
|
||||||
|
widget.elements[index] = updatedFormula;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _importSelected() {
|
||||||
|
final selectedElements = widget.elements.where((element) {
|
||||||
|
if (element is Formula) {
|
||||||
|
return _selectedUuids.contains(element.uuid);
|
||||||
|
} else if (element is UnitSpec) {
|
||||||
|
return _selectedUuids.contains(element.name);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
if (selectedElements.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(const SnackBar(content: Text('No elements selected to import'), backgroundColor: Colors.orange));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
widget.corpus.loadFormulaElements(selectedElements, true);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Imported ${selectedElements.length} element(s) successfully'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text('Error importing: $e'), backgroundColor: Colors.red));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final formulas = widget.elements.whereType<Formula>().toList();
|
||||||
|
final units = widget.elements.whereType<UnitSpec>().toList();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Import Preview'),
|
||||||
|
actions: [IconButton(icon: const Icon(Icons.check), tooltip: 'Import Selected', onPressed: _importSelected)],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
if (formulas.isEmpty && units.isEmpty)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: Text('No formula elements found in the shared content', style: TextStyle(fontSize: 16)),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
if (formulas.isNotEmpty) ...[
|
||||||
|
const ListTile(
|
||||||
|
title: Text('Formulas', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
...formulas.map((formula) => _buildFormulaTile(formula)),
|
||||||
|
],
|
||||||
|
if (units.isNotEmpty) ...[
|
||||||
|
const ListTile(
|
||||||
|
title: Text('Units', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
...units.map((unit) => _buildUnitTile(unit)),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFormulaTile(Formula formula) {
|
||||||
|
final isSelected = _selectedUuids.contains(formula.uuid);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
child: ListTile(
|
||||||
|
leading: Checkbox(
|
||||||
|
value: isSelected,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
if (value == true) {
|
||||||
|
_selectedUuids.add(formula.uuid);
|
||||||
|
} else {
|
||||||
|
_selectedUuids.remove(formula.uuid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(formula.name),
|
||||||
|
subtitle: Text(
|
||||||
|
formula.description?.isNotEmpty == true ? formula.description!.split('\n').first : 'No description',
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (formula.tags.isNotEmpty)
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 150,
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
children: formula.tags.take(10).map((tag) {
|
||||||
|
return Chip(
|
||||||
|
label: Text(tag, style: const TextStyle(fontSize: 10)),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(icon: const Icon(Icons.edit), tooltip: 'Edit', onPressed: () => _editFormulaElement(formula)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUnitTile(UnitSpec unit) {
|
||||||
|
final isSelected = _selectedUuids.contains(unit.name);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
child: ListTile(
|
||||||
|
leading: Checkbox(
|
||||||
|
value: isSelected,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
if (value == true) {
|
||||||
|
_selectedUuids.add(unit.name);
|
||||||
|
} else {
|
||||||
|
_selectedUuids.remove(unit.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(unit.name),
|
||||||
|
subtitle: Text('Base: ${unit.baseUnit} • Symbol: ${unit.symbol}'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -22,34 +22,33 @@ class UnitDropdown extends StatelessWidget {
|
||||||
final availableUnits = unitNames.map((name) => corpus.getUnit(name)).toList();
|
final availableUnits = unitNames.map((name) => corpus.getUnit(name)).toList();
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: 200, // Constrain dropdown width
|
width: 50, // Constrain dropdown width
|
||||||
child: DropdownButton<String>(
|
child: DropdownButton<String>(
|
||||||
value: selectedUnit ?? variable.unit,
|
value: selectedUnit ?? variable.unit,
|
||||||
selectedItemBuilder: (context) => availableUnits.map((unit) =>
|
selectedItemBuilder: (context) => availableUnits
|
||||||
SizedBox(
|
.map((unit) => SizedBox(width: 50, child: Text(unit.symbol, overflow: TextOverflow.ellipsis)))
|
||||||
width: 200,
|
.toList(),
|
||||||
child: Text(unit.symbol, overflow: TextOverflow.ellipsis),
|
icon: const Icon(Icons.arrow_drop_down),
|
||||||
)
|
elevation: 16,
|
||||||
).toList(),
|
style: TextStyle(color: Theme.of(context).colorScheme.primary, fontSize: 14),
|
||||||
icon: const Icon(Icons.arrow_drop_down),
|
underline: Container(height: 1, color: Theme.of(context).dividerColor),
|
||||||
elevation: 16,
|
onChanged: onUnitChanged,
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.primary, fontSize: 14),
|
items: availableUnits.map<DropdownMenuItem<String>>((UnitSpec unit) {
|
||||||
underline: Container(height: 1, color: Theme.of(context).dividerColor),
|
return DropdownMenuItem<String>(
|
||||||
onChanged: onUnitChanged,
|
value: unit.name,
|
||||||
items: availableUnits.map<DropdownMenuItem<String>>((UnitSpec unit) {
|
child: SizedBox(
|
||||||
return DropdownMenuItem<String>(
|
width: 300, // Fixed width for all items
|
||||||
value: unit.name,
|
child: Text(
|
||||||
child: SizedBox(
|
"${unit.symbol} - ${unit.name}",
|
||||||
width: 200, // Fixed width for all items
|
style: const TextStyle(fontSize: 14),
|
||||||
child: Text("${unit.symbol} - ${unit.name}",
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(fontSize: 14),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}).toList(),
|
||||||
}).toList(),
|
menuWidth: 300,
|
||||||
menuMaxHeight: 400,
|
menuMaxHeight: 400,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ class Corpus{
|
||||||
|
|
||||||
/// Loads formula elements, making sure to load units first, then formulas
|
/// Loads formula elements, making sure to load units first, then formulas
|
||||||
/// to avoid dependency issues.
|
/// to avoid dependency issues.
|
||||||
void loadFormulaElements(List<FormulaElement> elements) {
|
void loadFormulaElements(List<FormulaElement> elements, [bool replaceOnDuplicates = false]) {
|
||||||
List<UnitSpec> units = [];
|
List<UnitSpec> units = [];
|
||||||
List<Formula> formulas = [];
|
List<Formula> formulas = [];
|
||||||
|
|
||||||
|
|
@ -239,10 +239,10 @@ class Corpus{
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load units first to satisfy dependencies
|
// Load units first to satisfy dependencies
|
||||||
loadUnits(units);
|
loadUnits(units, replaceOnDuplicates);
|
||||||
|
|
||||||
// Then load formulas
|
// Then load formulas
|
||||||
loadFormulas(formulas);
|
loadFormulas(formulas, replaceOnDuplicates: replaceOnDuplicates, checkUnits: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads corpus from database elements
|
/// Loads corpus from database elements
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ extension CorpusDatabaseExtension on FormulasDatabase {
|
||||||
for (final element in elements) {
|
for (final element in elements) {
|
||||||
try {
|
try {
|
||||||
final parsed = SetUtils.parseCorpusElements('[${element.elementText}]');
|
final parsed = SetUtils.parseCorpusElements('[${element.elementText}]');
|
||||||
print("PARSED:$element");
|
|
||||||
parsedElements.addAll(parsed);
|
parsedElements.addAll(parsed);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error parsing database element: $e');
|
print('Error parsing database element: $e');
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:d4rt_formulas/d4rt_formulas.dart';
|
import 'package:d4rt_formulas/d4rt_formulas.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'ai/import_from_text_screen.dart';
|
||||||
import 'database/database_service.dart';
|
import 'database/database_service.dart';
|
||||||
import 'service_locator.dart';
|
import 'service_locator.dart';
|
||||||
|
|
||||||
|
|
@ -18,9 +19,13 @@ void main() async {
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final GlobalKey<_CorpusLoaderState> corpusLoaderKey = GlobalKey<_CorpusLoaderState>();
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
const MyApp({Key? key}) : super(key: key);
|
const MyApp({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
get corpusFuture => corpusLoaderKey.currentState?._corpusFuture;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
|
|
@ -30,19 +35,41 @@ class MyApp extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class CorpusLoader extends StatefulWidget {
|
class CorpusLoader extends StatefulWidget {
|
||||||
|
CorpusLoader({Key? key}) : super(key: corpusLoaderKey);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_CorpusLoaderState createState() => _CorpusLoaderState();
|
State<CorpusLoader> createState() => _CorpusLoaderState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CorpusLoaderState extends State<CorpusLoader> {
|
class _CorpusLoaderState extends State<CorpusLoader> {
|
||||||
late Future<Corpus> _corpusFuture;
|
late Future<Corpus> _corpusFuture;
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_corpusFuture = loadCorpusFromDatabaseOrAssets();
|
_corpusFuture = loadCorpusFromDatabaseOrAssets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleImport() {
|
||||||
|
_corpusFuture.then((corpus) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ImportFromTextScreen(
|
||||||
|
corpus: corpus,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
if (result) {
|
||||||
|
setState(() {
|
||||||
|
// Refresh the list when returning from import
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder<Corpus>(
|
return FutureBuilder<Corpus>(
|
||||||
|
|
@ -59,9 +86,19 @@ class _CorpusLoaderState extends State<CorpusLoader> {
|
||||||
// If the corpus is empty (user chose not to load default), we could handle that here
|
// If the corpus is empty (user chose not to load default), we could handle that here
|
||||||
// For now, just display the formula list
|
// For now, just display the formula list
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Formulas')),
|
appBar: AppBar(
|
||||||
|
title: const Text('Formulas'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.library_add),
|
||||||
|
tooltip: 'Import formulas',
|
||||||
|
onPressed: _handleImport,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
body: FormulaList(
|
body: FormulaList(
|
||||||
corpus: snapshot.data!,
|
corpus: snapshot.data!,
|
||||||
|
onImport: _handleImport,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
99
lib/services/import_service.dart
Normal file
99
lib/services/import_service.dart
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||||
|
import 'package:d4rt_formulas/formula_models.dart';
|
||||||
|
import 'package:d4rt_formulas/set_utils.dart';
|
||||||
|
import 'package:d4rt_formulas/error_handler.dart';
|
||||||
|
|
||||||
|
/// Service to handle import of formula elements from shared files or text
|
||||||
|
class ImportService {
|
||||||
|
static final ImportService _instance = ImportService._internal();
|
||||||
|
factory ImportService() => _instance;
|
||||||
|
ImportService._internal();
|
||||||
|
|
||||||
|
/// Parses shared text content as formula elements
|
||||||
|
/// The text should be in the same format as files in ./assets/formulas
|
||||||
|
List<FormulaElement> parseSharedText(String text) {
|
||||||
|
try {
|
||||||
|
final List<Object?> list = SetUtils.parseD4rtLiteral(text);
|
||||||
|
|
||||||
|
final elements = <FormulaElement>[];
|
||||||
|
for (final item in list) {
|
||||||
|
if (item is Map) {
|
||||||
|
// Try to parse as Formula first (has 'd4rtCode' field)
|
||||||
|
if (item.containsKey('d4rtCode')) {
|
||||||
|
elements.add(Formula.fromSet(item));
|
||||||
|
}
|
||||||
|
// Try to parse as UnitSpec (has 'name' and 'baseUnit' or 'isBase')
|
||||||
|
else if (item.containsKey('name')) {
|
||||||
|
elements.add(UnitSpec.fromSet(item));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw ArgumentError('Unknown element type: $item');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
} catch (e, stack) {
|
||||||
|
errorHandler.notify(e, stack);
|
||||||
|
throw FormatException('Failed to parse shared text as formula elements: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a .d4rtf file content as formula elements
|
||||||
|
List<FormulaElement> parseD4rtfFile(String filePath) {
|
||||||
|
try {
|
||||||
|
final file = File(filePath);
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
throw FileSystemException('File not found', filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
final content = file.readAsStringSync();
|
||||||
|
return parseSharedText(content);
|
||||||
|
} catch (e, stack) {
|
||||||
|
errorHandler.notify(e, stack);
|
||||||
|
throw FormatException('Failed to parse .d4rtf file: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Listens for shared files (Android only for now)
|
||||||
|
Stream<List<SharedMediaFile>> get sharedFilesStream {
|
||||||
|
return ReceiveSharingIntent.instance.getMediaStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets initial shared media (for when app is launched via share)
|
||||||
|
Future<List<SharedMediaFile>> getInitialSharedMedia() async {
|
||||||
|
try {
|
||||||
|
return await ReceiveSharingIntent.instance.getInitialMedia();
|
||||||
|
} catch (e, stack) {
|
||||||
|
errorHandler.notify(e, stack);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets shared text (for when app receives text via share)
|
||||||
|
Future<String?> getSharedText() async {
|
||||||
|
try {
|
||||||
|
final media = await ReceiveSharingIntent.instance.getInitialMedia();
|
||||||
|
// Note: In newer versions of receive_sharing_intent, TEXT type may not be available
|
||||||
|
// We check if media exists and try to get the path
|
||||||
|
if (media.isNotEmpty) {
|
||||||
|
return media.first.path;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e, stack) {
|
||||||
|
errorHandler.notify(e, stack);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the initial shared media after processing
|
||||||
|
Future<void> clearInitialSharedMedia() async {
|
||||||
|
try {
|
||||||
|
// Note: resetInitialMedia() was removed in newer versions
|
||||||
|
// The media is automatically cleared after being read
|
||||||
|
} catch (e, stack) {
|
||||||
|
errorHandler.notify(e, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,7 +37,6 @@ abstract class SetUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Escapes special characters in a string for use in D4RT literals
|
/// Escapes special characters in a string for use in D4RT literals
|
||||||
@deprecated
|
|
||||||
static String escapeD4rtString(String input) {
|
static String escapeD4rtString(String input) {
|
||||||
return input
|
return input
|
||||||
.replaceAll(r'\\', r'\\\\') // escape backslashes first
|
.replaceAll(r'\\', r'\\\\') // escape backslashes first
|
||||||
|
|
@ -75,9 +74,9 @@ abstract class SetUtils {
|
||||||
/// Uses JSON-like formatting but for Dart language, with proper indentation.
|
/// Uses JSON-like formatting but for Dart language, with proper indentation.
|
||||||
static String prettyPrint(dynamic value, {int indent = 0}) {
|
static String prettyPrint(dynamic value, {int indent = 0}) {
|
||||||
if (value is String) {
|
if (value is String) {
|
||||||
return _prettyPrintString(value, indent);
|
return _prettyPrintString(value);
|
||||||
} else if (value is num) {
|
} else if (value is num) {
|
||||||
return _prettyPrintNumber(value, indent);
|
return _prettyPrintNumber(value);
|
||||||
} else if (value is Set) {
|
} else if (value is Set) {
|
||||||
return _prettyPrintSet(value, indent);
|
return _prettyPrintSet(value, indent);
|
||||||
} else if (value is List) {
|
} else if (value is List) {
|
||||||
|
|
@ -90,15 +89,15 @@ abstract class SetUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pretty prints a simple string, escaping special characters if needed.
|
/// Pretty prints a simple string, escaping special characters if needed.
|
||||||
static String _prettyPrintString(String s, int indent) {
|
static String _prettyPrintString(String s) {
|
||||||
// Check if the string needs raw string formatting (newlines, $, backslashes, quotes)
|
// Check if the string needs raw string formatting (newlines, $, backslashes, quotes)
|
||||||
final needsRawString = s.contains('\n') ||
|
final needsRawString = s.contains('\n') ||
|
||||||
s.contains(r'$') ||
|
s.contains(r'$') ||
|
||||||
s.contains(r'\\') ||
|
s.contains(r'\\') ||
|
||||||
s.contains('"');
|
s.contains('"');
|
||||||
|
|
||||||
if (needsRawString) {
|
if (needsRawString && s != '"' ) {
|
||||||
return _prettyPrintRawString(s, indent);
|
return _prettyPrintRawString(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple string with escaped quotes
|
// Simple string with escaped quotes
|
||||||
|
|
@ -107,7 +106,7 @@ abstract class SetUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pretty prints a number.
|
/// Pretty prints a number.
|
||||||
static String _prettyPrintNumber(num n, int indent) {
|
static String _prettyPrintNumber(num n) {
|
||||||
return n.toString();
|
return n.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,9 +156,16 @@ abstract class SetUtils {
|
||||||
|
|
||||||
/// Pretty prints a raw string (for strings containing newlines, $, backslashes, etc.)
|
/// Pretty prints a raw string (for strings containing newlines, $, backslashes, etc.)
|
||||||
/// Uses Dart's raw string syntax r"""..."""
|
/// Uses Dart's raw string syntax r"""..."""
|
||||||
static String _prettyPrintRawString(String s, int indent) {
|
static String _prettyPrintRawString(String s) {
|
||||||
// Escape triple quotes by replacing """ with ""\"
|
if( s == '"'){
|
||||||
final escaped = s.replaceAll('"""', r'""\\"');
|
return "'\"";
|
||||||
return 'r"""$escaped"""';
|
}
|
||||||
|
if( s.contains('"""') && s.contains("'''") ){
|
||||||
|
return escapeD4rtString(s);
|
||||||
|
}
|
||||||
|
if( s.contains('"""') ){
|
||||||
|
return "r'''$s'''";
|
||||||
|
}
|
||||||
|
return 'r"""$s"""';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue