From d2295acc3b50f56cb592df5bdb62879ba2136aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Sun, 15 Mar 2026 11:31:53 +0100 Subject: [PATCH 01/12] onImport by qwen --- .gitignore | 1 + TODO.md | 8 ++++---- lib/ai/formula_list.dart | 21 +++++++++++++++++++++ lib/main.dart | 30 +++++++++++++++++++++++++++++- 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index d64610c..ff38daa 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ .aider* .build-container-cache /coverage/ +/.agent-shell/ diff --git a/TODO.md b/TODO.md index 1d86856..df8dbb0 100644 --- a/TODO.md +++ b/TODO.md @@ -67,15 +67,15 @@ - [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() +- [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 diff --git a/lib/ai/formula_list.dart b/lib/ai/formula_list.dart index 0974078..bfd4caf 100644 --- a/lib/ai/formula_list.dart +++ b/lib/ai/formula_list.dart @@ -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 @@ -133,6 +137,23 @@ class _FormulaListState extends State { ); } + void _importFromText() { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ImportFromTextScreen( + corpus: widget.corpus, + ), + ), + ).then((result) { + if (result == true) { + setState(() { + // Refresh the list when returning from import + }); + } + }); + } + @override Widget build(BuildContext context) { return Column( diff --git a/lib/main.dart b/lib/main.dart index 38b389e..549b962 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'ai/formula_list.dart'; import 'corpus.dart'; import 'defaults/default_corpus.dart'; import 'formula_models.dart' as models; +import 'ai/import_preview_screen.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -43,6 +44,23 @@ class _CorpusLoaderState extends State { _corpusFuture = loadCorpusFromDatabaseOrAssets(); } + void _handleImport() { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ImportFromTextScreen( + corpus: _corpusFuture.then((c) => c).value as Corpus? ?? Corpus(), + ), + ), + ).then((result) { + if( result ) { + setState(() { + // Refresh the list when returning from import + }); + } + }); + } + @override Widget build(BuildContext context) { return FutureBuilder( @@ -59,9 +77,19 @@ class _CorpusLoaderState extends State { // 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.import_export), + tooltip: 'Import formulas', + onPressed: _handleImport, + ), + ], + ), body: FormulaList( corpus: snapshot.data!, + onImport: _handleImport, ), ); } From 9b470041f5cb1ade0040606b981cfefff748bf34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Sun, 15 Mar 2026 11:34:04 +0100 Subject: [PATCH 02/12] Missed files --- android/d4rt_formulas_android.iml | 29 +++ lib/ai/import_preview_screen.dart | 390 ++++++++++++++++++++++++++++++ lib/services/import_service.dart | 95 ++++++++ 3 files changed, 514 insertions(+) create mode 100644 android/d4rt_formulas_android.iml create mode 100644 lib/ai/import_preview_screen.dart create mode 100644 lib/services/import_service.dart diff --git a/android/d4rt_formulas_android.iml b/android/d4rt_formulas_android.iml new file mode 100644 index 0000000..3bc4b3b --- /dev/null +++ b/android/d4rt_formulas_android.iml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/ai/import_preview_screen.dart b/lib/ai/import_preview_screen.dart new file mode 100644 index 0000000..7f0c3c5 --- /dev/null +++ b/lib/ai/import_preview_screen.dart @@ -0,0 +1,390 @@ +import 'package:flutter/material.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'; + +/// Screen to preview and import formula elements +class ImportPreviewScreen extends StatefulWidget { + final List elements; + final Corpus corpus; + + const ImportPreviewScreen({ + super.key, + required this.elements, + required this.corpus, + }); + + @override + State createState() => _ImportPreviewScreenState(); +} + +class _ImportPreviewScreenState extends State { + final Set _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); + + 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().toList(); + final units = widget.elements.whereType().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) + Wrap( + spacing: 4, + children: formula.tags.take(3).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}'), + ), + ); + } +} + +/// Screen to import formula elements from text +class ImportFromTextScreen extends StatefulWidget { + final Corpus corpus; + + const ImportFromTextScreen({ + super.key, + required this.corpus, + }); + + @override + State createState() => _ImportFromTextScreenState(); +} + +class _ImportFromTextScreenState extends State { + final TextEditingController _textController = TextEditingController(); + bool _isLoading = false; + + @override + void dispose() { + _textController.dispose(); + super.dispose(); + } + + Future _pasteFromClipboard() async { + setState(() => _isLoading = true); + + try { + final clipboardData = await Clipboard.getData('text/plain'); + if (clipboardData?.text != null) { + _textController.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 _import() async { + final text = _textController.text.trim(); + 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: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + controller: _textController, + decoration: const InputDecoration( + labelText: 'Paste formula text here', + hintText: 'Paste formula array literal in d4rt format...', + border: OutlineInputBorder(), + ), + maxLines: null, + expands: false, + ), + ), + 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.import_export), + label: const Text('Import'), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/services/import_service.dart b/lib/services/import_service.dart new file mode 100644 index 0000000..176ae00 --- /dev/null +++ b/lib/services/import_service.dart @@ -0,0 +1,95 @@ +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 parseSharedText(String text) { + try { + final List list = SetUtils.parseD4rtLiteral(text); + + final elements = []; + 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 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> get sharedFilesStream => + ReceiveSharingIntent.instance.getMediaStream; + + /// Gets initial shared media (for when app is launched via share) + Future> 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 getSharedText() async { + try { + final media = await ReceiveSharingIntent.instance.getInitialMedia(); + if (media.isNotEmpty && media.first.type == SharedMediaType.TEXT) { + return media.first.path; + } + return null; + } catch (e, stack) { + errorHandler.notify(e, stack); + return null; + } + } + + /// Clears the initial shared media after processing + Future clearInitialSharedMedia() async { + try { + ReceiveSharingIntent.instance.resetInitialMedia(); + } catch (e, stack) { + errorHandler.notify(e, stack); + } + } +} From d2bdf4a15723cd39abcf5dd44a5b4c8a03e59f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Tue, 17 Mar 2026 16:04:11 +0100 Subject: [PATCH 03/12] code is not full height, imported is not added to database --- .gitignore | 2 ++ lib/ai/import_preview_screen.dart | 1 + lib/main.dart | 26 ++++++++++++++------------ lib/services/import_service.dart | 12 ++++++++---- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index ff38daa..cfb2fe0 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ .build-container-cache /coverage/ /.agent-shell/ +/ios/ +/macos/ diff --git a/lib/ai/import_preview_screen.dart b/lib/ai/import_preview_screen.dart index 7f0c3c5..6ae312a 100644 --- a/lib/ai/import_preview_screen.dart +++ b/lib/ai/import_preview_screen.dart @@ -1,4 +1,5 @@ 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'; diff --git a/lib/main.dart b/lib/main.dart index 549b962..85a2812 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -45,19 +45,21 @@ class _CorpusLoaderState extends State { } void _handleImport() { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ImportFromTextScreen( - corpus: _corpusFuture.then((c) => c).value as Corpus? ?? Corpus(), + _corpusFuture.then((corpus) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ImportFromTextScreen( + corpus: corpus, + ), ), - ), - ).then((result) { - if( result ) { - setState(() { - // Refresh the list when returning from import - }); - } + ).then((result) { + if (result) { + setState(() { + // Refresh the list when returning from import + }); + } + }); }); } diff --git a/lib/services/import_service.dart b/lib/services/import_service.dart index 176ae00..6a425dd 100644 --- a/lib/services/import_service.dart +++ b/lib/services/import_service.dart @@ -57,8 +57,9 @@ class ImportService { } /// Listens for shared files (Android only for now) - Stream> get sharedFilesStream => - ReceiveSharingIntent.instance.getMediaStream; + Stream> get sharedFilesStream { + return ReceiveSharingIntent.instance.getMediaStream(); + } /// Gets initial shared media (for when app is launched via share) Future> getInitialSharedMedia() async { @@ -74,7 +75,9 @@ class ImportService { Future getSharedText() async { try { final media = await ReceiveSharingIntent.instance.getInitialMedia(); - if (media.isNotEmpty && media.first.type == SharedMediaType.TEXT) { + // 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; @@ -87,7 +90,8 @@ class ImportService { /// Clears the initial shared media after processing Future clearInitialSharedMedia() async { try { - ReceiveSharingIntent.instance.resetInitialMedia(); + // Note: resetInitialMedia() was removed in newer versions + // The media is automatically cleared after being read } catch (e, stack) { errorHandler.notify(e, stack); } From 26fc1bc51933f4d0224ff670abdde22da70a575d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Tue, 17 Mar 2026 19:15:06 +0100 Subject: [PATCH 04/12] better screen space, syntax highligthing --- lib/ai/import_preview_screen.dart | 36 +++++++++++++++++++------------ 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/lib/ai/import_preview_screen.dart b/lib/ai/import_preview_screen.dart index 6ae312a..7f83580 100644 --- a/lib/ai/import_preview_screen.dart +++ b/lib/ai/import_preview_screen.dart @@ -5,6 +5,11 @@ 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/java.dart'; + /// Screen to preview and import formula elements class ImportPreviewScreen extends StatefulWidget { final List elements; @@ -254,12 +259,15 @@ class ImportFromTextScreen extends StatefulWidget { } class _ImportFromTextScreenState extends State { - final TextEditingController _textController = TextEditingController(); + final CodeController _codeController = CodeController( + language: dart, + text: "// Insert code here...", + ); bool _isLoading = false; @override void dispose() { - _textController.dispose(); + _codeController.dispose(); super.dispose(); } @@ -269,7 +277,7 @@ class _ImportFromTextScreenState extends State { try { final clipboardData = await Clipboard.getData('text/plain'); if (clipboardData?.text != null) { - _textController.text = clipboardData!.text!; + _codeController.text = clipboardData!.text!; } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -291,7 +299,7 @@ class _ImportFromTextScreenState extends State { } Future _import() async { - final text = _textController.text.trim(); + final text = _codeController.fullText; if (text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -343,17 +351,17 @@ class _ImportFromTextScreenState extends State { ), body: Column( children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: TextField( - controller: _textController, - decoration: const InputDecoration( - labelText: 'Paste formula text here', - hintText: 'Paste formula array literal in d4rt format...', - border: OutlineInputBorder(), + Expanded( + child: CodeTheme( + data: CodeThemeData(styles: monokaiSublimeTheme), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: CodeField( + controller: _codeController, + ), + ), ), - maxLines: null, - expands: false, ), ), Padding( From 5108b405e08499faa36fd790d008acfd112aeb22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Tue, 17 Mar 2026 19:30:15 +0100 Subject: [PATCH 05/12] dart highlight --- lib/ai/import_preview_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ai/import_preview_screen.dart b/lib/ai/import_preview_screen.dart index 7f83580..143040f 100644 --- a/lib/ai/import_preview_screen.dart +++ b/lib/ai/import_preview_screen.dart @@ -8,7 +8,7 @@ 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/java.dart'; +import 'package:highlight/languages/dart.dart'; /// Screen to preview and import formula elements class ImportPreviewScreen extends StatefulWidget { From 3a5caece03b3e77bba854a9da2aeb7b024815376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Thu, 19 Mar 2026 11:25:59 +0100 Subject: [PATCH 06/12] changing layout sizes --- lib/ai/formula_screen.dart | 4 ++-- lib/ai/import_preview_screen.dart | 2 +- lib/ai/unit_dropdown.dart | 6 +++--- lib/corpus.dart | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/ai/formula_screen.dart b/lib/ai/formula_screen.dart index b982666..3d69408 100644 --- a/lib/ai/formula_screen.dart +++ b/lib/ai/formula_screen.dart @@ -388,10 +388,10 @@ class _FormulaScreenState extends State { 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 diff --git a/lib/ai/import_preview_screen.dart b/lib/ai/import_preview_screen.dart index 143040f..be9a11f 100644 --- a/lib/ai/import_preview_screen.dart +++ b/lib/ai/import_preview_screen.dart @@ -87,7 +87,7 @@ class _ImportPreviewScreenState extends State { } try { - widget.corpus.loadFormulaElements(selectedElements); + widget.corpus.loadFormulaElements(selectedElements, true); ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/ai/unit_dropdown.dart b/lib/ai/unit_dropdown.dart index 830b250..7d0975e 100644 --- a/lib/ai/unit_dropdown.dart +++ b/lib/ai/unit_dropdown.dart @@ -22,10 +22,10 @@ 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( value: selectedUnit ?? variable.unit, - selectedItemBuilder: (context) => availableUnits.map((unit) => + selectedItemBuilder: (context) => availableUnits.map((unit) => SizedBox( width: 200, child: Text(unit.symbol, overflow: TextOverflow.ellipsis), @@ -41,7 +41,7 @@ class UnitDropdown extends StatelessWidget { value: unit.name, child: SizedBox( width: 200, // Fixed width for all items - child: Text("${unit.symbol} - ${unit.name}", + child: Text("${unit.symbol} - ${unit.name}", style: const TextStyle(fontSize: 14), overflow: TextOverflow.ellipsis, ), diff --git a/lib/corpus.dart b/lib/corpus.dart index a3691c1..28c7cd1 100644 --- a/lib/corpus.dart +++ b/lib/corpus.dart @@ -223,7 +223,7 @@ class Corpus{ /// Loads formula elements, making sure to load units first, then formulas /// to avoid dependency issues. - void loadFormulaElements(List elements) { + void loadFormulaElements(List elements, [bool replaceOnDuplicates = false]) { List units = []; List 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 From 12d755d810fa5d60ececf688531223433fd871c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Thu, 19 Mar 2026 11:30:36 +0100 Subject: [PATCH 07/12] changing layout sizes --- lib/ai/unit_dropdown.dart | 47 +++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/lib/ai/unit_dropdown.dart b/lib/ai/unit_dropdown.dart index 7d0975e..b7a608d 100644 --- a/lib/ai/unit_dropdown.dart +++ b/lib/ai/unit_dropdown.dart @@ -25,31 +25,30 @@ class UnitDropdown extends StatelessWidget { width: 50, // Constrain dropdown width child: DropdownButton( 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>((UnitSpec unit) { - return DropdownMenuItem( - 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>((UnitSpec unit) { + return DropdownMenuItem( + 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, ), ); } From daebfeb3852395d28a18796c5deb9ab848975ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Thu, 19 Mar 2026 12:05:10 +0100 Subject: [PATCH 08/12] Changed icons and other minor changes --- lib/ai/formula_list.dart | 39 +------ lib/ai/formula_screen.dart | 15 ++- lib/ai/import_preview_screen.dart | 174 ++++++++++-------------------- lib/main.dart | 2 +- 4 files changed, 63 insertions(+), 167 deletions(-) diff --git a/lib/ai/formula_list.dart b/lib/ai/formula_list.dart index bfd4caf..bc4aa37 100644 --- a/lib/ai/formula_list.dart +++ b/lib/ai/formula_list.dart @@ -81,23 +81,6 @@ class _FormulaListState extends State { } } - 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); @@ -137,22 +120,6 @@ class _FormulaListState extends State { ); } - void _importFromText() { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ImportFromTextScreen( - corpus: widget.corpus, - ), - ), - ).then((result) { - if (result == true) { - setState(() { - // Refresh the list when returning from import - }); - } - }); - } @override Widget build(BuildContext context) { @@ -183,13 +150,9 @@ class _FormulaListState extends State { 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); diff --git a/lib/ai/formula_screen.dart b/lib/ai/formula_screen.dart index 3d69408..159e0fd 100644 --- a/lib/ai/formula_screen.dart +++ b/lib/ai/formula_screen.dart @@ -121,14 +121,13 @@ class _FormulaScreenState extends State { } 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 { children: [ // Fixed width for field name SizedBox( - width: 150, + width: 50, child: Text( formula.output.name, overflow: TextOverflow.ellipsis, diff --git a/lib/ai/import_preview_screen.dart b/lib/ai/import_preview_screen.dart index be9a11f..efb5288 100644 --- a/lib/ai/import_preview_screen.dart +++ b/lib/ai/import_preview_screen.dart @@ -5,7 +5,6 @@ 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'; @@ -15,11 +14,7 @@ class ImportPreviewScreen extends StatefulWidget { final List elements; final Corpus corpus; - const ImportPreviewScreen({ - super.key, - required this.elements, - required this.corpus, - }); + const ImportPreviewScreen({super.key, required this.elements, required this.corpus}); @override State createState() => _ImportPreviewScreenState(); @@ -52,9 +47,7 @@ class _ImportPreviewScreenState extends State { onSave: (updatedFormula) { // Update the element in the list setState(() { - final index = widget.elements.indexWhere( - (e) => e is Formula && e.uuid == updatedFormula.uuid, - ); + final index = widget.elements.indexWhere((e) => e is Formula && e.uuid == updatedFormula.uuid); if (index != -1) { widget.elements[index] = updatedFormula; } @@ -77,33 +70,27 @@ class _ImportPreviewScreenState extends State { }).toList(); if (selectedElements.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No elements selected to import'), - backgroundColor: Colors.orange, - ), - ); + 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, - ), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Error importing: $e'), backgroundColor: Colors.red)); } } @@ -115,23 +102,14 @@ class _ImportPreviewScreenState extends State { return Scaffold( appBar: AppBar( title: const Text('Import Preview'), - actions: [ - IconButton( - icon: const Icon(Icons.check), - tooltip: 'Import Selected', - onPressed: _importSelected, - ), - ], + 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), - ), + child: Text('No formula elements found in the shared content', style: TextStyle(fontSize: 16)), ) else Expanded( @@ -139,25 +117,13 @@ class _ImportPreviewScreenState extends State { children: [ if (formulas.isNotEmpty) ...[ const ListTile( - title: Text( - 'Formulas', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), + 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, - ), - ), + title: Text('Units', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), ), ...units.map((unit) => _buildUnitTile(unit)), ], @@ -171,7 +137,7 @@ class _ImportPreviewScreenState extends State { Widget _buildFormulaTile(Formula formula) { final isSelected = _selectedUuids.contains(formula.uuid); - + return Card( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: ListTile( @@ -189,9 +155,7 @@ class _ImportPreviewScreenState extends State { ), title: Text(formula.name), subtitle: Text( - formula.description?.isNotEmpty == true - ? formula.description!.split('\n').first - : 'No description', + formula.description?.isNotEmpty == true ? formula.description!.split('\n').first : 'No description', maxLines: 2, overflow: TextOverflow.ellipsis, ), @@ -199,21 +163,22 @@ class _ImportPreviewScreenState extends State { mainAxisSize: MainAxisSize.min, children: [ if (formula.tags.isNotEmpty) - Wrap( - spacing: 4, - children: formula.tags.take(3).map((tag) { - return Chip( - label: Text(tag, style: const TextStyle(fontSize: 10)), - padding: EdgeInsets.zero, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ); - }).toList(), + 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), - ), + IconButton(icon: const Icon(Icons.edit), tooltip: 'Edit', onPressed: () => _editFormulaElement(formula)), ], ), ), @@ -222,7 +187,7 @@ class _ImportPreviewScreenState extends State { Widget _buildUnitTile(UnitSpec unit) { final isSelected = _selectedUuids.contains(unit.name); - + return Card( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: ListTile( @@ -249,20 +214,14 @@ class _ImportPreviewScreenState extends State { class ImportFromTextScreen extends StatefulWidget { final Corpus corpus; - const ImportFromTextScreen({ - super.key, - required this.corpus, - }); + const ImportFromTextScreen({super.key, required this.corpus}); @override State createState() => _ImportFromTextScreenState(); } class _ImportFromTextScreenState extends State { - final CodeController _codeController = CodeController( - language: dart, - text: "// Insert code here...", - ); + final CodeController _codeController = CodeController(language: dart, text: "// Insert code here..."); bool _isLoading = false; @override @@ -273,26 +232,20 @@ class _ImportFromTextScreenState extends State { Future _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, - ), - ); + 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, - ), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Error pasting from clipboard: $e'), backgroundColor: Colors.red)); } finally { setState(() => _isLoading = false); } @@ -302,42 +255,33 @@ class _ImportFromTextScreenState extends State { final text = _codeController.fullText; if (text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Please enter or paste formula text'), - backgroundColor: Colors.orange, - ), + 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, - ), + 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, - ), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Error parsing text: $e'), backgroundColor: Colors.red)); } finally { setState(() => _isLoading = false); } @@ -346,9 +290,7 @@ class _ImportFromTextScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Import from Text'), - ), + appBar: AppBar(title: const Text('Import from Text')), body: Column( children: [ Expanded( @@ -356,11 +298,7 @@ class _ImportFromTextScreenState extends State { data: CodeThemeData(styles: monokaiSublimeTheme), child: Padding( padding: const EdgeInsets.all(16.0), - child: SingleChildScrollView( - child: CodeField( - controller: _codeController, - ), - ), + child: SingleChildScrollView(child: CodeField(controller: _codeController)), ), ), ), @@ -372,11 +310,7 @@ class _ImportFromTextScreenState extends State { child: ElevatedButton.icon( onPressed: _isLoading ? null : _pasteFromClipboard, icon: _isLoading - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) + ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.content_paste), label: const Text('Paste'), ), @@ -385,7 +319,7 @@ class _ImportFromTextScreenState extends State { Expanded( child: ElevatedButton.icon( onPressed: _isLoading ? null : _import, - icon: const Icon(Icons.import_export), + icon: const Icon(Icons.library_add), label: const Text('Import'), ), ), diff --git a/lib/main.dart b/lib/main.dart index 85a2812..16cc515 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -83,7 +83,7 @@ class _CorpusLoaderState extends State { title: const Text('Formulas'), actions: [ IconButton( - icon: const Icon(Icons.import_export), + icon: const Icon(Icons.library_add), tooltip: 'Import formulas', onPressed: _handleImport, ), From 842ce6acc95d5983bfc2544b14994ba441739bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Thu, 19 Mar 2026 18:47:02 +0100 Subject: [PATCH 09/12] selectable output variable --- lib/ai/formula_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ai/formula_screen.dart b/lib/ai/formula_screen.dart index 159e0fd..672d82c 100644 --- a/lib/ai/formula_screen.dart +++ b/lib/ai/formula_screen.dart @@ -352,7 +352,7 @@ class _FormulaScreenState extends State { Expanded( child: TextFormField( readOnly: true, - enabled: false, + enabled: true, controller: TextEditingController(text: _result), decoration: const InputDecoration( border: UnderlineInputBorder(), From 02fd849484f9c334445bb184414826e754a0f6cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Thu, 19 Mar 2026 18:52:33 +0100 Subject: [PATCH 10/12] import_from_text_screen.dart in its own file --- lib/ai/import_from_text_screen.dart | 140 ++++++++++++++++++++++++++++ lib/ai/import_preview_screen.dart | 121 ------------------------ lib/main.dart | 1 + 3 files changed, 141 insertions(+), 121 deletions(-) create mode 100644 lib/ai/import_from_text_screen.dart diff --git a/lib/ai/import_from_text_screen.dart b/lib/ai/import_from_text_screen.dart new file mode 100644 index 0000000..8219eb2 --- /dev/null +++ b/lib/ai/import_from_text_screen.dart @@ -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 createState() => _ImportFromTextScreenState(); +} + +class _ImportFromTextScreenState extends State { + final CodeController _codeController = CodeController(language: dart, text: "// Insert code here..."); + bool _isLoading = false; + + @override + void dispose() { + _codeController.dispose(); + super.dispose(); + } + + Future _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 _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'), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/ai/import_preview_screen.dart b/lib/ai/import_preview_screen.dart index efb5288..62ce1f4 100644 --- a/lib/ai/import_preview_screen.dart +++ b/lib/ai/import_preview_screen.dart @@ -210,124 +210,3 @@ class _ImportPreviewScreenState extends State { } } -/// Screen to import formula elements from text -class ImportFromTextScreen extends StatefulWidget { - final Corpus corpus; - - const ImportFromTextScreen({super.key, required this.corpus}); - - @override - State createState() => _ImportFromTextScreenState(); -} - -class _ImportFromTextScreenState extends State { - final CodeController _codeController = CodeController(language: dart, text: "// Insert code here..."); - bool _isLoading = false; - - @override - void dispose() { - _codeController.dispose(); - super.dispose(); - } - - Future _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 _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'), - ), - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/main.dart b/lib/main.dart index 16cc515..a31e947 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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'; From e49f6c307988748e461d4120b376119df6eb6b3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Sat, 21 Mar 2026 13:54:24 +0100 Subject: [PATCH 11/12] Code editor in formulaeditor --- CLAUDE.md | 1 + Makefile | 40 +++--- TODO.md | 3 +- lib/ai/formula_editor.dart | 215 ++++++++--------------------- lib/database/database_service.dart | 1 - lib/set_utils.dart | 28 ++-- 6 files changed, 98 insertions(+), 190 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fb77280..9d17b44 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/Makefile b/Makefile index bae5bf4..add553f 100644 --- a/Makefile +++ b/Makefile @@ -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 \ No newline at end of file + flutter run -d emulator-5554 diff --git a/TODO.md b/TODO.md index df8dbb0..b86211d 100644 --- a/TODO.md +++ b/TODO.md @@ -79,5 +79,6 @@ - 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). diff --git a/lib/ai/formula_editor.dart b/lib/ai/formula_editor.dart index 8020496..50f980b 100644 --- a/lib/ai/formula_editor.dart +++ b/lib/ai/formula_editor.dart @@ -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 createState() => _FormulaEditorState(); @@ -32,14 +30,14 @@ class _FormulaEditorState extends State { final _formKey = GlobalKey(); 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 { 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 { 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 { 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 { try { final input = []; 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 { 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 { 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 { 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 { 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 { 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 { 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 { final unitSpec = widget.corpus.getUnit(unit); return DropdownMenuItem( 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 { 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 { 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 { final unitSpec = widget.corpus.getUnit(unit); return DropdownMenuItem( 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 { 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? 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}); } diff --git a/lib/database/database_service.dart b/lib/database/database_service.dart index 532a38e..1767518 100644 --- a/lib/database/database_service.dart +++ b/lib/database/database_service.dart @@ -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'); diff --git a/lib/set_utils.dart b/lib/set_utils.dart index 2c2ca0b..26140e0 100644 --- a/lib/set_utils.dart +++ b/lib/set_utils.dart @@ -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"""'; } } \ No newline at end of file From 669c99e3a227148430b73b5bb2151509bb976b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Sat, 21 Mar 2026 13:54:56 +0100 Subject: [PATCH 12/12] lost file --- lib/main.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index a31e947..e1da391 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,7 +9,6 @@ import 'ai/formula_list.dart'; import 'corpus.dart'; import 'defaults/default_corpus.dart'; import 'formula_models.dart' as models; -import 'ai/import_preview_screen.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -20,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( @@ -32,13 +35,16 @@ class MyApp extends StatelessWidget { } class CorpusLoader extends StatefulWidget { + CorpusLoader({Key? key}) : super(key: corpusLoaderKey); + @override - _CorpusLoaderState createState() => _CorpusLoaderState(); + State createState() => _CorpusLoaderState(); } class _CorpusLoaderState extends State { late Future _corpusFuture; + @override void initState() { super.initState();