Merge branch 'feature/import-formulas'

This commit is contained in:
Álvaro González 2026-03-21 13:55:19 +01:00
commit a7178e2e81
16 changed files with 667 additions and 257 deletions

3
.gitignore vendored
View file

@ -18,3 +18,6 @@
.aider*
.build-container-cache
/coverage/
/.agent-shell/
/ios/
/macos/

View file

@ -9,6 +9,7 @@
- `flutter pub get` --> `./flutterw pub get`
- `flutter run -d linux` --> `./flutterw run -d linux`
- See `./Makefile` for more examples.
- If you are an agent, you may be also containerized. Try `distrobox-host-exec $(pwd)/flutterw`
# MANDATORY WORKFLOW

View file

@ -1,43 +1,45 @@
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:
./flutterw --build-container
$(FLUTTERW) --build-container
clean:
flutter clean
[ -f $(DB) ] && rm $(DB)
[ -f $(DATABASEFILE) ] && rm $(DATABASEFILE)
clean-container:
rm -r .build-container-cache
./flutterw clean
$(FLUTTERW) clean
pub-get-container:
./flutterw pub get
$(FLUTTERW) pub get
test:
./flutterw test
test:
$(FLUTTERW) test
build-builders:
./flutterw pub run build_runner build --delete-conflicting-outputs
$(FLUTTERW) pub run build_runner build --delete-conflicting-outputs
build-android-release-container:
./flutterw build apk --release
build-android-release-container:
$(FLUTTERW) build apk --release
build-linux-debug-container:
./flutterw build linux --debug
build-linux-debug-container:
$(FLUTTERW) build linux --debug
build-web-debug-container:
./flutterw build web --debug
build-web-debug-container:
$(FLUTTERW) build web --debug
run-linux-debug-container:
./flutterw run -d linux
run-linux-debug-container:
$(FLUTTERW) run -d linux
run-web-debug-container:
./flutterw run --web-port $${WEB_PORT:-8081} -d web-server
run-web-debug-container:
$(FLUTTERW) run --web-port $${WEB_PORT:-8081} -d web-server
run-linux-debug-native:
flutter run -d linux
@ -50,4 +52,4 @@ ai:
run-emulator:
flutter emulators --launch Medium_Phone
flutter run -d emulator-5554
flutter run -d emulator-5554

11
TODO.md
View file

@ -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] 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
- [ ] 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 also shared text, with same format as files in ./assets.
- The loaded formulaelemets will be added to the GetIt.instance.get<Corpus)()
- [ ] Preview of imported formulalements
- The loaded formulaelemets will be added to the GetIt.instance.get<Corpus>()
- [R] Preview of imported formulalements
- 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 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".
- The "paste" button will copy the clipboard into the text editor.
- 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.
- [ ] 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).

View 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>

View file

@ -9,6 +9,9 @@ import '../database/database_service.dart';
import '../service_locator.dart';
import 'formula_screen.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,
/// input/output variables, and d4rt code.
@ -17,12 +20,7 @@ class FormulaEditor extends StatefulWidget {
final Corpus corpus;
final Function(Formula)? onSave;
const FormulaEditor({
super.key,
required this.formula,
required this.corpus,
this.onSave,
});
const FormulaEditor({super.key, required this.formula, required this.corpus, this.onSave});
@override
State<FormulaEditor> createState() => _FormulaEditorState();
@ -32,14 +30,14 @@ class _FormulaEditorState extends State<FormulaEditor> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nameController;
late TextEditingController _descriptionController;
late TextEditingController _d4rtCodeController;
late CodeController _d4rtCodeController;
// Track input variables
final List<_InputVariableRowData> _inputVariables = [];
// Output variable
late _OutputVariableRowData _outputVariable;
bool _isPreviewVisible = false;
@override
@ -47,17 +45,19 @@ class _FormulaEditorState extends State<FormulaEditor> {
super.initState();
_nameController = TextEditingController(text: widget.formula.name);
_descriptionController = TextEditingController(text: widget.formula.description ?? '');
_d4rtCodeController = TextEditingController(text: widget.formula.d4rtCode);
_d4rtCodeController = CodeController(language: dart, text: widget.formula.d4rtCode ?? '');
// Initialize input variables
for (final input in widget.formula.input) {
_inputVariables.add(_InputVariableRowData(
nameController: TextEditingController(text: input.name),
unit: input.unit,
values: input.values != null ? List.from(input.values!) : null,
));
_inputVariables.add(
_InputVariableRowData(
nameController: TextEditingController(text: input.name),
unit: input.unit,
values: input.values != null ? List.from(input.values!) : null,
),
);
}
// Initialize output variable
_outputVariable = _OutputVariableRowData(
nameController: TextEditingController(text: widget.formula.output.name),
@ -79,11 +79,13 @@ class _FormulaEditorState extends State<FormulaEditor> {
void _addInputVariable() {
setState(() {
_inputVariables.add(_InputVariableRowData(
nameController: TextEditingController(text: 'var${_inputVariables.length + 1}'),
unit: null,
values: null,
));
_inputVariables.add(
_InputVariableRowData(
nameController: TextEditingController(text: 'var${_inputVariables.length + 1}'),
unit: null,
values: null,
),
);
});
}
@ -117,10 +119,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FormulaScreen(
formula: formula,
corpus: widget.corpus,
),
builder: (context) => FormulaScreen(formula: formula, corpus: widget.corpus),
),
);
}
@ -159,17 +158,12 @@ class _FormulaEditorState extends State<FormulaEditor> {
try {
final input = <VariableSpec>[];
for (final variable in _inputVariables) {
input.add(VariableSpec(
name: variable.nameController.text.trim(),
unit: variable.unit,
values: variable.values,
));
input.add(
VariableSpec(name: variable.nameController.text.trim(), unit: variable.unit, values: variable.values),
);
}
final output = VariableSpec(
name: _outputVariable.nameController.text.trim(),
unit: _outputVariable.unit,
);
final output = VariableSpec(name: _outputVariable.nameController.text.trim(), unit: _outputVariable.unit);
return Formula(
uuid: widget.formula.uuid,
@ -177,7 +171,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
description: _descriptionController.text.isEmpty ? null : _descriptionController.text,
input: input,
output: output,
d4rtCode: _d4rtCodeController.text,
d4rtCode: _d4rtCodeController.fullText,
tags: widget.formula.tags, // Preserve existing tags
);
} catch (e) {
@ -296,21 +290,9 @@ class _FormulaEditorState extends State<FormulaEditor> {
appBar: AppBar(
title: const Text('Edit Formula'),
actions: [
IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: _testFormula,
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',
),
IconButton(icon: const Icon(Icons.play_arrow), onPressed: _testFormula, 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(
@ -363,13 +345,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Description (Markdown)',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const Text('Description (Markdown)', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Row(
mainAxisSize: MainAxisSize.min,
children: [
@ -400,13 +376,8 @@ class _FormulaEditorState extends State<FormulaEditor> {
child: Markdown(
data: _descriptionController.text,
shrinkWrap: true,
builders: {
'latex': LatexElementBuilder(),
},
extensionSet: markdown.ExtensionSet(
[LatexBlockSyntax()],
[LatexInlineSyntax()],
),
builders: {'latex': LatexElementBuilder()},
extensionSet: markdown.ExtensionSet([LatexBlockSyntax()], [LatexInlineSyntax()]),
),
),
const SizedBox(height: 16),
@ -432,13 +403,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Input Variables',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const Text('Input Variables', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
..._inputVariables.asMap().entries.map((entry) {
final index = entry.key;
@ -470,10 +435,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
flex: 2,
child: TextFormField(
controller: variable.nameController,
decoration: const InputDecoration(
labelText: 'Name',
border: OutlineInputBorder(),
),
decoration: const InputDecoration(labelText: 'Name', border: OutlineInputBorder()),
),
),
const SizedBox(width: 8),
@ -530,7 +492,8 @@ class _FormulaEditorState extends State<FormulaEditor> {
final unitSpec = widget.corpus.getUnit(unit);
return DropdownMenuItem<String?>(
value: unit,
child: Text('${unitSpec.symbol} - ${unit}',
child: Text(
'${unitSpec.symbol} - ${unit}',
style: const TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
),
@ -567,13 +530,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Output Variable',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const Text('Output Variable', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Row(
children: [
@ -581,10 +538,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
flex: 2,
child: TextFormField(
controller: _outputVariable.nameController,
decoration: const InputDecoration(
labelText: 'Name',
border: OutlineInputBorder(),
),
decoration: const InputDecoration(labelText: 'Name', border: OutlineInputBorder()),
),
),
const SizedBox(width: 8),
@ -641,7 +595,8 @@ class _FormulaEditorState extends State<FormulaEditor> {
final unitSpec = widget.corpus.getUnit(unit);
return DropdownMenuItem<String?>(
value: unit,
child: Text('${unitSpec.symbol} - ${unit}',
child: Text(
'${unitSpec.symbol} - ${unit}',
style: const TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
),
@ -667,67 +622,18 @@ class _FormulaEditorState extends State<FormulaEditor> {
Widget _buildD4rtCodeSection() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'D4RT Code',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
child: Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: CodeTheme(
data: CodeThemeData(styles: monokaiSublimeTheme),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SingleChildScrollView(
child: CodeField(controller: _d4rtCodeController),
),
),
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;
List<dynamic>? values;
_InputVariableRowData({
required this.nameController,
this.unit,
this.values,
});
_InputVariableRowData({required this.nameController, this.unit, this.values});
}
class _OutputVariableRowData {
final TextEditingController nameController;
String? unit;
_OutputVariableRowData({
required this.nameController,
this.unit,
});
_OutputVariableRowData({required this.nameController, this.unit});
}

View file

@ -7,13 +7,17 @@ import 'formula_screen.dart';
import 'package:share_plus/share_plus.dart' as share_plus;
import 'formula_editor.dart';
import 'package:share_plus/share_plus.dart';
import 'import_preview_screen.dart';
import '../services/import_service.dart';
class FormulaList extends StatefulWidget {
final Corpus corpus;
final VoidCallback? onImport;
const FormulaList({
super.key,
required this.corpus,
this.onImport,
});
@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 {
try {
final exportString = _formulaAndDependenciesToExportStringLiteral(formula);
@ -133,6 +120,7 @@ class _FormulaListState extends State<FormulaList> {
);
}
@override
Widget build(BuildContext context) {
return Column(
@ -162,13 +150,9 @@ class _FormulaListState extends State<FormulaList> {
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _editFormula(formula),
tooltip: 'Edit Formula',
),
PopupMenuButton(
icon: const Icon(Icons.share),
tooltip: 'Share or copy to clipboard',
onSelected: (value) {
if (value == 'share') {
_shareFormula(formula);

View file

@ -121,14 +121,13 @@ class _FormulaScreenState extends State<FormulaScreen> {
}
late final dynamic result;
//if( formula is DerivedFormula) {
if( formula is DerivedFormula) {
result = formulaSolver(formula, formula.output.name, inputValues,);
//}
//else {
// TODO: MAYBE ONLY FORMULASOLVER IS NECCESSARY"
//final evaluator = FormulaEvaluator();
//result = evaluator.evaluate(formula as Formula, inputValues);
//}
}
else {
final evaluator = FormulaEvaluator();
result = evaluator.evaluate(formula as Formula, inputValues);
}
// Convert output to selected unit if needed
String? unit = formula.output.unit;
@ -342,7 +341,7 @@ class _FormulaScreenState extends State<FormulaScreen> {
children: [
// Fixed width for field name
SizedBox(
width: 150,
width: 50,
child: Text(
formula.output.name,
overflow: TextOverflow.ellipsis,
@ -353,7 +352,7 @@ class _FormulaScreenState extends State<FormulaScreen> {
Expanded(
child: TextFormField(
readOnly: true,
enabled: false,
enabled: true,
controller: TextEditingController(text: _result),
decoration: const InputDecoration(
border: UnderlineInputBorder(),
@ -388,10 +387,10 @@ class _FormulaScreenState extends State<FormulaScreen> {
children: [
// Fixed width for field name
SizedBox(
width: 150,
width: 50,
child: Text(
variable.name,
overflow: TextOverflow.ellipsis,
overflow: TextOverflow.fade
),
),
const SizedBox(width: 8), // Add some spacing

View 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'),
),
),
],
),
),
],
),
);
}
}

View 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}'),
),
);
}
}

View file

@ -22,34 +22,33 @@ class UnitDropdown extends StatelessWidget {
final availableUnits = unitNames.map((name) => corpus.getUnit(name)).toList();
return SizedBox(
width: 200, // Constrain dropdown width
width: 50, // Constrain dropdown width
child: DropdownButton<String>(
value: selectedUnit ?? variable.unit,
selectedItemBuilder: (context) => availableUnits.map((unit) =>
SizedBox(
width: 200,
child: Text(unit.symbol, overflow: TextOverflow.ellipsis),
)
).toList(),
icon: const Icon(Icons.arrow_drop_down),
elevation: 16,
style: TextStyle(color: Theme.of(context).colorScheme.primary, fontSize: 14),
underline: Container(height: 1, color: Theme.of(context).dividerColor),
onChanged: onUnitChanged,
items: availableUnits.map<DropdownMenuItem<String>>((UnitSpec unit) {
return DropdownMenuItem<String>(
value: unit.name,
child: SizedBox(
width: 200, // Fixed width for all items
child: Text("${unit.symbol} - ${unit.name}",
style: const TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
selectedItemBuilder: (context) => availableUnits
.map((unit) => SizedBox(width: 50, child: Text(unit.symbol, overflow: TextOverflow.ellipsis)))
.toList(),
icon: const Icon(Icons.arrow_drop_down),
elevation: 16,
style: TextStyle(color: Theme.of(context).colorScheme.primary, fontSize: 14),
underline: Container(height: 1, color: Theme.of(context).dividerColor),
onChanged: onUnitChanged,
items: availableUnits.map<DropdownMenuItem<String>>((UnitSpec unit) {
return DropdownMenuItem<String>(
value: unit.name,
child: SizedBox(
width: 300, // Fixed width for all items
child: Text(
"${unit.symbol} - ${unit.name}",
style: const TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
),
),
),
);
}).toList(),
menuMaxHeight: 400,
isExpanded: true,
);
}).toList(),
menuWidth: 300,
menuMaxHeight: 400,
isExpanded: true,
),
);
}

View file

@ -223,7 +223,7 @@ class Corpus{
/// Loads formula elements, making sure to load units first, then formulas
/// to avoid dependency issues.
void loadFormulaElements(List<FormulaElement> elements) {
void loadFormulaElements(List<FormulaElement> elements, [bool replaceOnDuplicates = false]) {
List<UnitSpec> units = [];
List<Formula> formulas = [];
@ -239,10 +239,10 @@ class Corpus{
}
// Load units first to satisfy dependencies
loadUnits(units);
loadUnits(units, replaceOnDuplicates);
// Then load formulas
loadFormulas(formulas);
loadFormulas(formulas, replaceOnDuplicates: replaceOnDuplicates, checkUnits: true);
}
/// Loads corpus from database elements

View file

@ -14,7 +14,6 @@ extension CorpusDatabaseExtension on FormulasDatabase {
for (final element in elements) {
try {
final parsed = SetUtils.parseCorpusElements('[${element.elementText}]');
print("PARSED:$element");
parsedElements.addAll(parsed);
} catch (e) {
print('Error parsing database element: $e');

View file

@ -1,6 +1,7 @@
import 'package:d4rt_formulas/d4rt_formulas.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'ai/import_from_text_screen.dart';
import 'database/database_service.dart';
import 'service_locator.dart';
@ -18,9 +19,13 @@ void main() async {
runApp(const MyApp());
}
final GlobalKey<_CorpusLoaderState> corpusLoaderKey = GlobalKey<_CorpusLoaderState>();
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
get corpusFuture => corpusLoaderKey.currentState?._corpusFuture;
@override
Widget build(BuildContext context) {
return MaterialApp(
@ -30,19 +35,41 @@ class MyApp extends StatelessWidget {
}
class CorpusLoader extends StatefulWidget {
CorpusLoader({Key? key}) : super(key: corpusLoaderKey);
@override
_CorpusLoaderState createState() => _CorpusLoaderState();
State<CorpusLoader> createState() => _CorpusLoaderState();
}
class _CorpusLoaderState extends State<CorpusLoader> {
late Future<Corpus> _corpusFuture;
@override
void initState() {
super.initState();
_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
Widget build(BuildContext context) {
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
// For now, just display the formula list
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(
corpus: snapshot.data!,
onImport: _handleImport,
),
);
}

View 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);
}
}
}

View file

@ -37,7 +37,6 @@ abstract class SetUtils {
}
/// Escapes special characters in a string for use in D4RT literals
@deprecated
static String escapeD4rtString(String input) {
return input
.replaceAll(r'\\', r'\\\\') // escape backslashes first
@ -75,9 +74,9 @@ abstract class SetUtils {
/// Uses JSON-like formatting but for Dart language, with proper indentation.
static String prettyPrint(dynamic value, {int indent = 0}) {
if (value is String) {
return _prettyPrintString(value, indent);
return _prettyPrintString(value);
} else if (value is num) {
return _prettyPrintNumber(value, indent);
return _prettyPrintNumber(value);
} else if (value is Set) {
return _prettyPrintSet(value, indent);
} else if (value is List) {
@ -90,15 +89,15 @@ abstract class SetUtils {
}
/// 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)
final needsRawString = s.contains('\n') ||
s.contains(r'$') ||
s.contains(r'\\') ||
s.contains('"');
if (needsRawString) {
return _prettyPrintRawString(s, indent);
if (needsRawString && s != '"' ) {
return _prettyPrintRawString(s);
}
// Simple string with escaped quotes
@ -107,7 +106,7 @@ abstract class SetUtils {
}
/// Pretty prints a number.
static String _prettyPrintNumber(num n, int indent) {
static String _prettyPrintNumber(num n) {
return n.toString();
}
@ -157,9 +156,16 @@ abstract class SetUtils {
/// Pretty prints a raw string (for strings containing newlines, $, backslashes, etc.)
/// Uses Dart's raw string syntax r"""..."""
static String _prettyPrintRawString(String s, int indent) {
// Escape triple quotes by replacing """ with ""\"
final escaped = s.replaceAll('"""', r'""\\"');
return 'r"""$escaped"""';
static String _prettyPrintRawString(String s) {
if( s == '"'){
return "'\"";
}
if( s.contains('"""') && s.contains("'''") ){
return escapeD4rtString(s);
}
if( s.contains('"""') ){
return "r'''$s'''";
}
return 'r"""$s"""';
}
}