Merge branch 'feature/share-button'
This commit is contained in:
commit
88d7802bd5
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 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<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().
|
||||
- [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.
|
||||
- [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
|
||||
|
|
|
|||
|
|
@ -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<FormulaList> {
|
|||
|
||||
List<Formula> 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<FormulaList> {
|
|||
}).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
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
|
|
@ -72,9 +141,41 @@ class _FormulaListState extends State<FormulaList> {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,16 @@ List<Object?> 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<FormulaElement> 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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
40
pubspec.lock
40
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:
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ dependencies:
|
|||
ffi: ^2.0.1
|
||||
|
||||
collection: any
|
||||
share_plus: ^10.0.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
|
|
|||
|
|
@ -6,10 +6,13 @@
|
|||
|
||||
#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 <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
share_plus
|
||||
sqlite3_flutter_libs
|
||||
url_launcher_windows
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue