Missed files

This commit is contained in:
Álvaro González 2026-03-15 11:34:04 +01:00
parent d2295acc3b
commit 9b470041f5
3 changed files with 514 additions and 0 deletions

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android" name="Android">
<configuration>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="GEN_FOLDER_RELATIVE_PATH_APT" value="/gen" />
<option name="GEN_FOLDER_RELATIVE_PATH_AIDL" value="/gen" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/app/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/app/src/main/res" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/app/src/main/assets" />
<option name="LIBS_FOLDER_RELATIVE_PATH" value="/app/src/main/libs" />
<option name="PROGUARD_LOGS_FOLDER_RELATIVE_PATH" value="/app/src/main/proguard_logs" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/app/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/app/src/main/kotlin" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
</content>
<orderEntry type="jdk" jdkName="Android API 24 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Flutter for Android" level="project" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
</component>
</module>

View file

@ -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<FormulaElement> elements;
final Corpus corpus;
const ImportPreviewScreen({
super.key,
required this.elements,
required this.corpus,
});
@override
State<ImportPreviewScreen> createState() => _ImportPreviewScreenState();
}
class _ImportPreviewScreenState extends State<ImportPreviewScreen> {
final Set<String> _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<Formula>().toList();
final units = widget.elements.whereType<UnitSpec>().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<ImportFromTextScreen> createState() => _ImportFromTextScreenState();
}
class _ImportFromTextScreenState extends State<ImportFromTextScreen> {
final TextEditingController _textController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_textController.dispose();
super.dispose();
}
Future<void> _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<void> _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'),
),
),
],
),
),
],
),
);
}
}

View file

@ -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<FormulaElement> parseSharedText(String text) {
try {
final List<Object?> list = SetUtils.parseD4rtLiteral(text);
final elements = <FormulaElement>[];
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<FormulaElement> 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<List<SharedMediaFile>> get sharedFilesStream =>
ReceiveSharingIntent.instance.getMediaStream;
/// Gets initial shared media (for when app is launched via share)
Future<List<SharedMediaFile>> 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<String?> 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<void> clearInitialSharedMedia() async {
try {
ReceiveSharingIntent.instance.resetInitialMedia();
} catch (e, stack) {
errorHandler.notify(e, stack);
}
}
}