From 9b470041f5cb1ade0040606b981cfefff748bf34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Sun, 15 Mar 2026 11:34:04 +0100 Subject: [PATCH] Missed files --- android/d4rt_formulas_android.iml | 29 +++ lib/ai/import_preview_screen.dart | 390 ++++++++++++++++++++++++++++++ lib/services/import_service.dart | 95 ++++++++ 3 files changed, 514 insertions(+) create mode 100644 android/d4rt_formulas_android.iml create mode 100644 lib/ai/import_preview_screen.dart create mode 100644 lib/services/import_service.dart diff --git a/android/d4rt_formulas_android.iml b/android/d4rt_formulas_android.iml new file mode 100644 index 0000000..3bc4b3b --- /dev/null +++ b/android/d4rt_formulas_android.iml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/ai/import_preview_screen.dart b/lib/ai/import_preview_screen.dart new file mode 100644 index 0000000..7f0c3c5 --- /dev/null +++ b/lib/ai/import_preview_screen.dart @@ -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 elements; + final Corpus corpus; + + const ImportPreviewScreen({ + super.key, + required this.elements, + required this.corpus, + }); + + @override + State createState() => _ImportPreviewScreenState(); +} + +class _ImportPreviewScreenState extends State { + final Set _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().toList(); + final units = widget.elements.whereType().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 createState() => _ImportFromTextScreenState(); +} + +class _ImportFromTextScreenState extends State { + final TextEditingController _textController = TextEditingController(); + bool _isLoading = false; + + @override + void dispose() { + _textController.dispose(); + super.dispose(); + } + + Future _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 _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'), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/services/import_service.dart b/lib/services/import_service.dart new file mode 100644 index 0000000..176ae00 --- /dev/null +++ b/lib/services/import_service.dart @@ -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 parseSharedText(String text) { + try { + final List list = SetUtils.parseD4rtLiteral(text); + + final elements = []; + 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 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> get sharedFilesStream => + ReceiveSharingIntent.instance.getMediaStream; + + /// Gets initial shared media (for when app is launched via share) + Future> 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 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 clearInitialSharedMedia() async { + try { + ReceiveSharingIntent.instance.resetInitialMedia(); + } catch (e, stack) { + errorHandler.notify(e, stack); + } + } +}