diff --git a/Makefile b/Makefile index 39fe39a..cbb4ae2 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -all: clean-container build-builders build-linux-debug-container +all: build-container clean-container build-builders build-linux-debug-container DB=~/.local/share/com.example.d4rt_formulas/d4rt_formulas/formulas.sqlite @@ -10,17 +10,18 @@ clean: flutter clean [ -f $(DB) ] && rm $(DB) -clean-container: build-container +clean-container: + rm -r .build-container-cache ./flutterw clean - rm .build-container-cache -pub-get-container: build-container + +pub-get-container: ./flutterw pub get test: ./flutterw test -build-builders: build-container +build-builders: ./flutterw pub run build_runner build --delete-conflicting-outputs build-android-release-container: diff --git a/TODO.md b/TODO.md index 8993abb..c0154ca 100644 --- a/TODO.md +++ b/TODO.md @@ -57,7 +57,6 @@ - A constructor without UUID will generate a new random UUID. A constructor with UUID will use the provided UUID. - The field should be used in database and everywhere instead of the name. The name is not unique anymore, but the UUID is. - This will be used to identify formulas, instead of the name. This way, we can have formulas with the same name but different UUIDs. The name is not unique anymore. Corpus will be a list of UUIDs, instead of a list of formulas. The corpus.getFormula() method will return the first formula with that name. -- [ ] When _FormulaScreenState._evaluateFormula() detect an error, instead of show an SnackBar, show a ExpansionTile with "⚠️ There were an error. Show details..." with the details of the exception. The ExpansionTile will be invisible if there is no error. +- [R] When _FormulaScreenState._evaluateFormula() detect an error, instead of show an SnackBar, show a ExpansionTile with "⚠️ There were an error. Show details..." with the details of the exception. The ExpansionTile will be invisible if there is no error. - [R] When FormulaEditor._save formula, ensure formula is updated in the initial FormulaList -- [ ] Refresh FormulaList each time it gets focus, so formulas are updated from corpus -- [ ] Investigate https://pub.dev/packages/quantity +- [R] In FormulaEditor, add a button to "Save as copy", additional to the existing button "Save". It doesnt matter if the copy has the same name as the original formula, since they are identified by a internal UUID. diff --git a/assets/formulas/it-networking.d4rt b/assets/formulas/it-networking.d4rt index 3baaac3..14fa848 100644 --- a/assets/formulas/it-networking.d4rt +++ b/assets/formulas/it-networking.d4rt @@ -15,23 +15,25 @@ The network address is calculated by applying the subnet mask to zero out the ho {"name": "hostIP", "unit": "string"} ], "output": {"name": "network", "unit": "string"}, - "d4rtCode": r"""var parts = hostIP.split('/'); -var ip = parts[0]; -var mask = int.parse(parts[1]); -var octets = ip.split('.').map((e) => int.parse(e)).toList(); -var hostBits = 32 - mask; -var shiftAmount = hostBits; -var networkValue = 0; -for (var i = 0; i < 4; i++) { - networkValue = (networkValue << 8) | octets[i]; -} -networkValue = (networkValue >> shiftAmount) << shiftAmount; -var networkOctets = []; -for (var i = 0; i < 4; i++) { - networkOctets.insert(0, networkValue & 0xFF); - networkValue = networkValue >> 8; -} -network = networkOctets.join('.') + '/' + mask.toString();""", + "d4rtCode": r""" + var parts = hostIP.split('/'); + var ip = parts[0]; + var mask = int.parse(parts[1]); + var octets = ip.split('.').map((e) => int.parse(e)).toList(); + var hostBits = 32 - mask; + var shiftAmount = hostBits; + var networkValue = 0; + for (var i = 0; i < 4; i++) { + networkValue = (networkValue << 8) | octets[i]; + } + networkValue = (networkValue >> shiftAmount) << shiftAmount; + var networkOctets = []; + for (var i = 0; i < 4; i++) { + networkOctets.insert(0, networkValue & 0xFF); + networkValue = networkValue >> 8; + } + network = networkOctets.join('.') + '/' + mask.toString(); + """, "tags": ["networking", "ip", "subnetting", "cidr", "network"] } ] diff --git a/assets/formulas/networking.d4rt b/assets/formulas/networking.d4rt deleted file mode 100644 index ceda27e..0000000 --- a/assets/formulas/networking.d4rt +++ /dev/null @@ -1,83 +0,0 @@ -[ - // IP Subnet and Broadcast Calculator - { - "name": "IP Subnet and Broadcast", - "description": r""" -Calculates the network (subnet) address and broadcast address for an IPv4 address with CIDR notation. - -**Input format:** `ip_address/prefix` where: -- `ip_address`: IPv4 address in dotted decimal notation (e.g., `192.168.1.100`) -- `prefix`: CIDR prefix length (1-30) or subnet mask in dotted notation (e.g., `24` or `255.255.255.0`) - -**Output:** -- `subnet`: Network address in dotted decimal notation -- `broadcast`: Broadcast address in dotted decimal notation - -**Examples:** -- Input: `192.168.1.100/24` → Subnet: `192.168.1.0`, Broadcast: `192.168.1.255` -- Input: `10.0.0.50/8` → Subnet: `10.0.0.0`, Broadcast: `10.255.255.255` -- Input: `172.16.5.100/16` → Subnet: `172.16.0.0`, Broadcast: `172.16.255.255`""", - "input": [ - {"name": "ipWithMask", "unit": "scalar"} - ], - "output": {"name": "subnet", "unit": "scalar"}, - "d4rtCode": """ - var input = ipWithMask.toString(); - var slashIndex = input.indexOf('/'); - if (slashIndex == -1) { - subnet = 'error: no / found'; - broadcast = ''; - } else { - var ipPart = input.substring(0, slashIndex).trim(); - var maskPart = input.substring(slashIndex + 1).trim(); - - // Parse IP address - var ipParts = ipPart.split('.'); - if (ipParts.length != 4) { - subnet = 'error: invalid IP'; - broadcast = ''; - } else { - var octet1 = int.parse(ipParts[0]); - var octet2 = int.parse(ipParts[1]); - var octet3 = int.parse(ipParts[2]); - var octet4 = int.parse(ipParts[3]); - - // Convert IP to 32-bit integer - var ipInt = (octet1 << 24) | (octet2 << 16) | (octet3 << 8) | octet4; - - // Parse mask (CIDR prefix or dotted notation) - int maskInt; - if (maskPart.contains('.')) { - var maskParts = maskPart.split('.'); - var m1 = int.parse(maskParts[0]); - var m2 = int.parse(maskParts[1]); - var m3 = int.parse(maskParts[2]); - var m4 = int.parse(maskParts[3]); - maskInt = (m1 << 24) | (m2 << 16) | (m3 << 8) | m4; - } else { - var prefix = int.parse(maskPart); - maskInt = prefix == 0 ? 0 : (-1 << (32 - prefix)); - } - - // Calculate subnet and broadcast - var subnetInt = ipInt & maskInt; - var broadcastInt = subnetInt | (~maskInt & 0xFFFFFFFF); - - // Convert back to dotted notation - var s1 = (subnetInt >> 24) & 0xFF; - var s2 = (subnetInt >> 16) & 0xFF; - var s3 = (subnetInt >> 8) & 0xFF; - var s4 = subnetInt & 0xFF; - subnet = '\$s1.\$s2.\$s3.\$s4'; - - var b1 = (broadcastInt >> 24) & 0xFF; - var b2 = (broadcastInt >> 16) & 0xFF; - var b3 = (broadcastInt >> 8) & 0xFF; - var b4 = broadcastInt & 0xFF; - broadcast = '\$b1.\$b2.\$b3.\$b4'; - } - } - """, - "tags": ["networking", "ip", "subnet", "broadcast", "cidr"] - } -] diff --git a/lib/ai/formula_editor.dart b/lib/ai/formula_editor.dart index 895e82b..8020496 100644 --- a/lib/ai/formula_editor.dart +++ b/lib/ai/formula_editor.dart @@ -196,21 +196,21 @@ class _FormulaEditorState extends State { try { final database = getDatabase(); - + // Update corpus in memory widget.corpus.updateFormula(formula); - + // Update database final updated = await database.updateFormula(formula); - + if (!updated) { // If formula wasn't found (e.g., name changed), add it as new await database.addFormula(formula); } - + // Call the onSave callback if provided widget.onSave?.call(formula); - + // Show success message ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -224,6 +224,52 @@ class _FormulaEditorState extends State { } } + Future _saveFormulaAsCopy() async { + if (!_validateFormula()) { + return; + } + + final formula = _buildFormula(); + if (formula == null) return; + + try { + final database = getDatabase(); + + // Create a copy with a new UUID + final formulaCopy = Formula( + name: '${formula.name} (Copy)', + description: formula.description, + input: formula.input, + output: formula.output, + d4rtCode: formula.d4rtCode, + tags: formula.tags, + ); + + // Add to corpus + widget.corpus.addFormula(formulaCopy); + + // Add to database + await database.addFormula(formulaCopy); + + // Call the onSave callback if provided + widget.onSave?.call(formulaCopy); + + // Show success message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Formula "${formulaCopy.name}" saved successfully!'), + backgroundColor: Theme.of(context).colorScheme.primary, + ), + ); + + // Navigate back to the formula list with the new formula + Navigator.pop(context, formulaCopy); + } catch (e, stack) { + print('Error saving formula copy: $e\n$stack'); + _showErrorDialog('Error saving formula copy: $e'); + } + } + void _showErrorDialog(String message) { showDialog( context: context, @@ -255,6 +301,11 @@ class _FormulaEditorState extends State { 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, diff --git a/lib/ai/formula_screen.dart b/lib/ai/formula_screen.dart index 6064752..1e6fd8f 100644 --- a/lib/ai/formula_screen.dart +++ b/lib/ai/formula_screen.dart @@ -34,6 +34,8 @@ class _FormulaScreenState extends State { late Formula _formula; Formula get formula => _formula; + String? _errorMessage; // Track error message for expansion tile + bool _isErrorExpanded = false; // Track error expansion state set formula(Formula newFormula) { _formula = newFormula; @@ -126,18 +128,16 @@ class _FormulaScreenState extends State { _result = result?.toString(); } - setState(() {}); + setState(() { + _errorMessage = null; // Clear error on successful evaluation + }); } catch (e, stack) { errorHandler.notify(e, stack); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: ${e.toString()}'), - backgroundColor: Theme - .of(context) - .colorScheme - .error, - ), - ); + setState(() { + _errorMessage = e.toString(); + _isErrorExpanded = true; // Auto-expand on error + _result = null; + }); } } @@ -178,6 +178,7 @@ class _FormulaScreenState extends State { child: ListView( children: [ _buildDescriptionSection(), + _buildErrorSection(), _buildInputSection(), const SizedBox(height: 24), _buildOutputSection(), @@ -243,6 +244,51 @@ class _FormulaScreenState extends State { ); } + Widget _buildErrorSection() { + if (_errorMessage == null) { + return const SizedBox.shrink(); + } + + return Card( + margin: const EdgeInsets.only(bottom: 16), + color: Theme.of(context).colorScheme.errorContainer, + child: ExpansionTile( + title: Text( + '⚠️ There were an error. Show details...', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), + initiallyExpanded: _isErrorExpanded, + onExpansionChanged: (bool expanded) { + setState(() { + _isErrorExpanded = expanded; + }); + }, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(8), + ), + child: SelectableText( + _errorMessage!, + style: TextStyle( + color: Theme.of(context).colorScheme.onErrorContainer, + fontFamily: 'monospace', + ), + ), + ), + ), + ], + ), + ); + } + Widget _buildInputSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/corpus.dart b/lib/corpus.dart index f6939b5..a3691c1 100644 --- a/lib/corpus.dart +++ b/lib/corpus.dart @@ -77,25 +77,32 @@ class Corpus{ /// Updates a formula in the corpus void updateFormula(Formula formula) { if (!_allFormulas.containsKey(formula.uuid)) { - _allFormulas.keys.forEach( (uuid)=> print("Existing formula uuid: $uuid, name: ${_allFormulas[uuid]?.name}") ); - throw ArgumentError("Formula not found: ${formula.name} ${formula.uuid}"); + throw ArgumentError("Formula not found: ${formula.uuid}"); } // Remove old tags final oldFormula = _allFormulas[formula.uuid]!; for (final tag in oldFormula.tags) { - _tags[tag]?.removeWhere((f) => f.name == formula.name); + _tags[tag]?.removeWhere((f) => f.uuid == formula.uuid); } // Update the formula _allFormulas[formula.uuid] = formula; - + // Add new tags for (final tag in formula.tags) { _tags[tag]?.add(formula); } } + /// Adds a new formula to the corpus + void addFormula(Formula formula) { + _allFormulas[formula.uuid] = formula; + for (final tag in formula.tags) { + _tags[tag]?.add(formula); + } + } + final Multimap _baseToUnits = Multimap.create(); final Map _allUnits = {}; diff --git a/lib/database/database_service.dart b/lib/database/database_service.dart index de3ab49..dbb07a3 100644 --- a/lib/database/database_service.dart +++ b/lib/database/database_service.dart @@ -37,10 +37,10 @@ extension CorpusDatabaseExtension on FormulasDatabase { } } - // Method to update a formula in the database by name + // Method to update a formula in the database by UUID Future updateFormula(models.Formula formula) async { final elements = await getAllFormulaElements(); - + for (final element in elements) { try { final parsed = SetUtils.parseCorpusElements('[${element.elementText}]'); @@ -49,7 +49,7 @@ extension CorpusDatabaseExtension on FormulasDatabase { if (existingFormula.uuid == formula.uuid) { // Update this element await updateFormulaElement( - element.id, + element.id, formula.toStringLiteral() ); return true; @@ -60,7 +60,7 @@ extension CorpusDatabaseExtension on FormulasDatabase { continue; } } - + return false; // Formula not found } diff --git a/lib/defaults/default_corpus.dart b/lib/defaults/default_corpus.dart index 10057f1..b9c095c 100644 --- a/lib/defaults/default_corpus.dart +++ b/lib/defaults/default_corpus.dart @@ -63,7 +63,6 @@ Future createDefaultCorpus() async{ "assets/formulas/materials_elasticity.d4rt", "assets/formulas/medical_and_bio.d4rt", "assets/formulas/misc_math.d4rt", - "assets/formulas/networking.d4rt", "assets/formulas/optics.d4rt", "assets/formulas/thermodynamics.d4rt", "assets/formulas/trigonometry.d4rt",