From 05fd37dd9a5ac0f13b4d92d4c93263c78ef8e2e7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 15 Feb 2026 12:10:48 +0100 Subject: [PATCH 1/2] Add share functionality and improve string escaping in FormulaElement.toStringLiteral - Add share button to formula list with export functionality - Implement proper escaping of special characters (\n, \t, \", etc.) in FormulaElement.toStringLiteral methods - Create escapeD4rtString helper function for consistent escaping - Update Formula, UnitSpec, and VariableSpec toStringLiteral methods to use escaping - Add share_plus package dependency for sharing functionality Co-authored-by: Qwen-Coder --- TODO.md | 4 +- lib/ai/formula_list.dart | 105 +++++++++++++++++- lib/formula_models.dart | 59 ++++++---- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 40 +++++++ pubspec.yaml | 2 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 8 files changed, 193 insertions(+), 23 deletions(-) diff --git a/TODO.md b/TODO.md index 8742036..40b27f5 100644 --- a/TODO.md +++ b/TODO.md @@ -28,6 +28,6 @@ - [X] If the database is empty, sugest to use a default corpus - [X] If the user choose to use the default corpus, populate de database with the default corpus (load defaultcorpus, and then use toStringLiteral). If not, start with an empty list of formulas. - [X] From now on, the corpus will be loaded from database instead of assets -- [R] Create method List Corpus.withDependencies(Formula formula). It will return the formula, the units of the formula, and all the units from the corpus with the same base unit. -- [ ] Add a Share button to the formula list. It will export the array string literal of the formula with the units from Corpus.withDependencies(). +- [X] Create method List Corpus.withDependencies(Formula formula). It will return the formula, the units of the formula, and all the units from the corpus with the same base unit. +- [R] Add a Share button to the formula list. It will export the array string literal of the formula with the units from Corpus.withDependencies(). - [ ] Replace flutter-markdown with flutter-markdown-plus diff --git a/lib/ai/formula_list.dart b/lib/ai/formula_list.dart index 803f667..5348710 100644 --- a/lib/ai/formula_list.dart +++ b/lib/ai/formula_list.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; // For Clipboard import 'package:d4rt_formulas/formula_models.dart'; import '../corpus.dart'; import 'formula_screen.dart'; +import 'package:share_plus/share_plus.dart'; class FormulaList extends StatefulWidget { final Corpus corpus; @@ -41,7 +43,7 @@ class _FormulaListState extends State { List get _filteredFormulas { if (_searchQuery.isEmpty) return widget.formulas; - + return widget.formulas.where((formula) { final nameMatch = formula.name.toLowerCase().contains(_searchQuery); final tagMatch = formula.tags.any((tag) => tag.toLowerCase().contains(_searchQuery)); @@ -49,6 +51,73 @@ class _FormulaListState extends State { }).toList(); } + void _shareFormula(Formula formula) async { + try { + // Get the formula and its dependencies + final dependencies = widget.corpus.withDependencies(formula); + + // Convert each dependency to its string literal representation + final literals = dependencies.map((element) => element.toStringLiteral()).toList(); + + // Create an array string literal containing all the elements + final exportString = '[${literals.join(', ')}]'; + + // Share the string + await Share.share( + exportString, + subject: 'Sharing formula: ${formula.name}', + ); + } catch (e) { + _showErrorDialog('Error sharing formula: $e'); + } + } + + void _copyFormula(Formula formula) async { + try { + // Get the formula and its dependencies + final dependencies = widget.corpus.withDependencies(formula); + + // Convert each dependency to its string literal representation + final literals = dependencies.map((element) => element.toStringLiteral()).toList(); + + // Create an array string literal containing all the elements + final exportString = '[${literals.join(', ')}]'; + + // Copy to clipboard + await Clipboard.setData(ClipboardData(text: exportString)); + + // Show a snackbar to confirm + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Formula and dependencies copied to clipboard!'), + duration: Duration(seconds: 2), + ), + ); + } catch (e) { + _showErrorDialog('Error copying formula: $e'); + } + } + + void _showErrorDialog(String message) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Error'), + content: Text(message), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text('OK'), + ), + ], + ); + }, + ); + } + @override Widget build(BuildContext context) { return Column( @@ -72,9 +141,41 @@ class _FormulaListState extends State { final formula = _filteredFormulas[index]; return ListTile( title: Text(formula.name), - subtitle: formula.tags.isNotEmpty + subtitle: formula.tags.isNotEmpty ? Text('Tags: ${formula.tags.join(', ')}') : null, + trailing: PopupMenuButton( + icon: Icon(Icons.share), + onSelected: (value) { + if (value == 'share') { + _shareFormula(formula); + } else if (value == 'copy') { + _copyFormula(formula); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'share', + child: Row( + children: [ + Icon(Icons.share), + SizedBox(width: 8), + Text('Share'), + ], + ), + ), + PopupMenuItem( + value: 'copy', + child: Row( + children: [ + Icon(Icons.copy), + SizedBox(width: 8), + Text('Copy to clipboard'), + ], + ), + ), + ], + ), onTap: () { Navigator.push( context, diff --git a/lib/formula_models.dart b/lib/formula_models.dart index af1c692..677695d 100644 --- a/lib/formula_models.dart +++ b/lib/formula_models.dart @@ -36,6 +36,16 @@ List parseD4rtLiteral(String arrayStringLiteral) { return list; } +/// Escapes special characters in a string for use in D4RT literals +String escapeD4rtString(String input) { + return input + .replaceAll(r'\', r'\\') // Escape backslashes first + .replaceAll('\n', r'\n') // Escape newlines + .replaceAll('\r', r'\r') // Escape carriage returns + .replaceAll('\t', r'\t') // Escape tabs + .replaceAll('"', r'\"'); // Escape quotes last +} + /// Parses corpus elements from an array string literal. /// Determines if each element is a formula or a unit and converts accordingly. List parseCorpusElements(String arrayStringLiteral) { @@ -143,21 +153,21 @@ class UnitSpec implements FormulaElement { @override String toStringLiteral() { final buffer = StringBuffer('{'); - buffer.write('"name": "$name", "symbol": "$symbol"'); - + buffer.write('"name": "${escapeD4rtString(name)}", "symbol": "${escapeD4rtString(symbol)}"'); + if (name == baseUnit && factorFromUnitToBase == 1) { // This is a base unit buffer.write(', "isBase": true'); } else { - buffer.write(', "baseUnit": "$baseUnit"'); - + buffer.write(', "baseUnit": "${escapeD4rtString(baseUnit)}"'); + if (factorFromUnitToBase != null) { buffer.write(', "factor": $factorFromUnitToBase'); } else if (codeFromUnitToBase != null && codeFromBaseToUnit != null) { - buffer.write(', "toBase": "$codeFromUnitToBase", "fromBase": "$codeFromBaseToUnit"'); + buffer.write(', "toBase": "${escapeD4rtString(codeFromUnitToBase!)}", "fromBase": "${escapeD4rtString(codeFromBaseToUnit!)}"'); } } - + buffer.write('}'); return buffer.toString(); } @@ -200,22 +210,22 @@ class VariableSpec { @override String toStringLiteral() { final buffer = StringBuffer('{'); - buffer.write('"name": "$name"'); - + buffer.write('"name": "${escapeD4rtString(name)}"'); + if (unit != null) { - buffer.write(', "unit": "$unit"'); + buffer.write(', "unit": "${escapeD4rtString(unit!)}"'); } - + if (values != null && values!.isNotEmpty) { buffer.write(', "values": [${values!.map((value) { if (value is String) { - return '"$value"'; + return '"${escapeD4rtString(value)}"'; } else { return value.toString(); } }).join(", ")}]'); } - + buffer.write('}'); return buffer.toString(); } @@ -337,22 +347,33 @@ class Formula implements FormulaElement { /// by the D4RT parser to recreate the same Formula object. String toStringLiteral() { final inputStrings = input.map((varSpec) => varSpec.toStringLiteral()).toList(); - + final buffer = StringBuffer('{'); buffer.write('"name": "$name"'); - + if (description != null) { - buffer.write(', "description": "$description"'); + buffer.write(', "description": "${escapeD4rtString(description!)}"'); } - + buffer.write(', "input": [${inputStrings.join(", ")}]'); buffer.write(', "output": ${output.toStringLiteral()}'); - buffer.write(', "d4rtCode": ${d4rtCode.contains('\n') || d4rtCode.contains('"') ? 'r"""$d4rtCode"""' : '"$d4rtCode"'}'); - if (tags.isNotEmpty) { - buffer.write(', "tags": [${tags.map((tag) => '"$tag"').join(", ")}]'); + // Handle d4rtCode with proper escaping + String escapedD4rtCode; + if (d4rtCode.contains('\n') || d4rtCode.contains('"')) { + // For multiline strings or strings with quotes, use raw string but still escape internal quotes + escapedD4rtCode = 'r"""${d4rtCode.replaceAll('"', '\\"')}"""'; + } else { + // For single-line strings, use escaped version + escapedD4rtCode = '"${escapeD4rtString(d4rtCode)}"'; } + buffer.write(', "d4rtCode": $escapedD4rtCode'); + + if (tags.isNotEmpty) { + buffer.write(', "tags": [${tags.map((tag) => '"${escapeD4rtString(tag)}"').join(", ")}]'); + } + buffer.write('}'); return buffer.toString(); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index eeb37f4..a9d0f33 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation +import share_plus import sqlite3_flutter_libs import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 8b23f76..b5fafa4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -201,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" crypto: dependency: transitive description: @@ -752,6 +760,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.8" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da + url: "https://pub.dev" + source: hosted + version: "10.1.4" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b + url: "https://pub.dev" + source: hosted + version: "5.0.2" shelf: dependency: transitive description: @@ -997,6 +1021,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.5" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" vector_graphics: dependency: transitive description: @@ -1077,6 +1109,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" xdg_directories: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b51444d..e32c40f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,6 +51,8 @@ dependencies: ffi: ^2.0.1 collection: any + share_plus: ^10.0.3 + dev_dependencies: flutter_test: sdk: flutter diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 76d5285..0143d6e 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,10 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 22aeaae..b707726 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + share_plus sqlite3_flutter_libs url_launcher_windows ) From db7ac04c1c5a2828e7faf427d6aafe3fdce488ad Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 15 Feb 2026 20:29:49 +0100 Subject: [PATCH 2/2] share button marked done --- TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index 40b27f5..579bbc0 100644 --- a/TODO.md +++ b/TODO.md @@ -29,5 +29,5 @@ - [X] If the user choose to use the default corpus, populate de database with the default corpus (load defaultcorpus, and then use toStringLiteral). If not, start with an empty list of formulas. - [X] From now on, the corpus will be loaded from database instead of assets - [X] Create method List Corpus.withDependencies(Formula formula). It will return the formula, the units of the formula, and all the units from the corpus with the same base unit. -- [R] Add a Share button to the formula list. It will export the array string literal of the formula with the units from Corpus.withDependencies(). +- [X] Add a Share button to the formula list. It will export the array string literal of the formula with the units from Corpus.withDependencies(). - [ ] Replace flutter-markdown with flutter-markdown-plus