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 <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
8b5529dddc
commit
05fd37dd9a
8 changed files with 193 additions and 23 deletions
4
TODO.md
4
TODO.md
|
|
@ -28,6 +28,6 @@
|
||||||
- [X] If the database is empty, sugest to use a default corpus
|
- [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] 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] From now on, the corpus will be loaded from database instead of assets
|
||||||
- [R] Create method List<FormulaElement> 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.
|
- [X] Create method List<FormulaElement> 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().
|
- [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
|
- [ ] Replace flutter-markdown with flutter-markdown-plus
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart'; // For Clipboard
|
||||||
import 'package:d4rt_formulas/formula_models.dart';
|
import 'package:d4rt_formulas/formula_models.dart';
|
||||||
import '../corpus.dart';
|
import '../corpus.dart';
|
||||||
import 'formula_screen.dart';
|
import 'formula_screen.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
class FormulaList extends StatefulWidget {
|
class FormulaList extends StatefulWidget {
|
||||||
final Corpus corpus;
|
final Corpus corpus;
|
||||||
|
|
@ -49,6 +51,73 @@ class _FormulaListState extends State<FormulaList> {
|
||||||
}).toList();
|
}).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: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
|
|
@ -75,6 +144,38 @@ class _FormulaListState extends State<FormulaList> {
|
||||||
subtitle: formula.tags.isNotEmpty
|
subtitle: formula.tags.isNotEmpty
|
||||||
? Text('Tags: ${formula.tags.join(', ')}')
|
? Text('Tags: ${formula.tags.join(', ')}')
|
||||||
: null,
|
: 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: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,16 @@ List<Object?> parseD4rtLiteral(String arrayStringLiteral) {
|
||||||
return list;
|
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.
|
/// Parses corpus elements from an array string literal.
|
||||||
/// Determines if each element is a formula or a unit and converts accordingly.
|
/// Determines if each element is a formula or a unit and converts accordingly.
|
||||||
List<FormulaElement> parseCorpusElements(String arrayStringLiteral) {
|
List<FormulaElement> parseCorpusElements(String arrayStringLiteral) {
|
||||||
|
|
@ -143,18 +153,18 @@ class UnitSpec implements FormulaElement {
|
||||||
@override
|
@override
|
||||||
String toStringLiteral() {
|
String toStringLiteral() {
|
||||||
final buffer = StringBuffer('{');
|
final buffer = StringBuffer('{');
|
||||||
buffer.write('"name": "$name", "symbol": "$symbol"');
|
buffer.write('"name": "${escapeD4rtString(name)}", "symbol": "${escapeD4rtString(symbol)}"');
|
||||||
|
|
||||||
if (name == baseUnit && factorFromUnitToBase == 1) {
|
if (name == baseUnit && factorFromUnitToBase == 1) {
|
||||||
// This is a base unit
|
// This is a base unit
|
||||||
buffer.write(', "isBase": true');
|
buffer.write(', "isBase": true');
|
||||||
} else {
|
} else {
|
||||||
buffer.write(', "baseUnit": "$baseUnit"');
|
buffer.write(', "baseUnit": "${escapeD4rtString(baseUnit)}"');
|
||||||
|
|
||||||
if (factorFromUnitToBase != null) {
|
if (factorFromUnitToBase != null) {
|
||||||
buffer.write(', "factor": $factorFromUnitToBase');
|
buffer.write(', "factor": $factorFromUnitToBase');
|
||||||
} else if (codeFromUnitToBase != null && codeFromBaseToUnit != null) {
|
} else if (codeFromUnitToBase != null && codeFromBaseToUnit != null) {
|
||||||
buffer.write(', "toBase": "$codeFromUnitToBase", "fromBase": "$codeFromBaseToUnit"');
|
buffer.write(', "toBase": "${escapeD4rtString(codeFromUnitToBase!)}", "fromBase": "${escapeD4rtString(codeFromBaseToUnit!)}"');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,16 +210,16 @@ class VariableSpec {
|
||||||
@override
|
@override
|
||||||
String toStringLiteral() {
|
String toStringLiteral() {
|
||||||
final buffer = StringBuffer('{');
|
final buffer = StringBuffer('{');
|
||||||
buffer.write('"name": "$name"');
|
buffer.write('"name": "${escapeD4rtString(name)}"');
|
||||||
|
|
||||||
if (unit != null) {
|
if (unit != null) {
|
||||||
buffer.write(', "unit": "$unit"');
|
buffer.write(', "unit": "${escapeD4rtString(unit!)}"');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values != null && values!.isNotEmpty) {
|
if (values != null && values!.isNotEmpty) {
|
||||||
buffer.write(', "values": [${values!.map((value) {
|
buffer.write(', "values": [${values!.map((value) {
|
||||||
if (value is String) {
|
if (value is String) {
|
||||||
return '"$value"';
|
return '"${escapeD4rtString(value)}"';
|
||||||
} else {
|
} else {
|
||||||
return value.toString();
|
return value.toString();
|
||||||
}
|
}
|
||||||
|
|
@ -342,15 +352,26 @@ class Formula implements FormulaElement {
|
||||||
buffer.write('"name": "$name"');
|
buffer.write('"name": "$name"');
|
||||||
|
|
||||||
if (description != null) {
|
if (description != null) {
|
||||||
buffer.write(', "description": "$description"');
|
buffer.write(', "description": "${escapeD4rtString(description!)}"');
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.write(', "input": [${inputStrings.join(", ")}]');
|
buffer.write(', "input": [${inputStrings.join(", ")}]');
|
||||||
buffer.write(', "output": ${output.toStringLiteral()}');
|
buffer.write(', "output": ${output.toStringLiteral()}');
|
||||||
buffer.write(', "d4rtCode": ${d4rtCode.contains('\n') || d4rtCode.contains('"') ? 'r"""$d4rtCode"""' : '"$d4rtCode"'}');
|
|
||||||
|
// 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) {
|
if (tags.isNotEmpty) {
|
||||||
buffer.write(', "tags": [${tags.map((tag) => '"$tag"').join(", ")}]');
|
buffer.write(', "tags": [${tags.map((tag) => '"${escapeD4rtString(tag)}"').join(", ")}]');
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.write('}');
|
buffer.write('}');
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@
|
||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import share_plus
|
||||||
import sqlite3_flutter_libs
|
import sqlite3_flutter_libs
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
40
pubspec.lock
40
pubspec.lock
|
|
@ -201,6 +201,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.0"
|
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:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -752,6 +760,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.8"
|
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:
|
shelf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -997,6 +1021,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.5"
|
version: "3.1.5"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.2"
|
||||||
vector_graphics:
|
vector_graphics:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1077,6 +1109,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.2.1"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.15.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@ dependencies:
|
||||||
ffi: ^2.0.1
|
ffi: ^2.0.1
|
||||||
|
|
||||||
collection: any
|
collection: any
|
||||||
|
share_plus: ^10.0.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,13 @@
|
||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||||
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
|
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
share_plus
|
||||||
sqlite3_flutter_libs
|
sqlite3_flutter_libs
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue