diff --git a/src/backend/models/block.go b/src/backend/models/block.go index e6e62405..06b064c3 100644 --- a/src/backend/models/block.go +++ b/src/backend/models/block.go @@ -13,6 +13,7 @@ import ( type BlockType string const ( + // TODO: Align block types / remove unneded TextBlock BlockType = "text" TaskBlock BlockType = "task" HeadingBlock BlockType = "header" diff --git a/src/frontend/lib/providers/note_editor_provider.dart b/src/frontend/lib/providers/note_editor_provider.dart index 7a500567..ea23a53f 100644 --- a/src/frontend/lib/providers/note_editor_provider.dart +++ b/src/frontend/lib/providers/note_editor_provider.dart @@ -447,12 +447,12 @@ class NoteEditorProvider with ChangeNotifier implements NoteEditorViewModel { } // DocumentChangeListener implementation for content changes - void _documentChangeListener(dynamic _) { + void _documentChangeListener(dynamic changelog) { if (_updatingDocument) return; // Check if this change is a DocumentChangeLog which might contain TaskNode changes - if (_ is DocumentChangeLog) { - DocumentChangeLog changeLog = _; + if (changelog is DocumentChangeLog) { + DocumentChangeLog changeLog = changelog; // Check if this change includes a TaskNode's isComplete property change bool hasTaskStateChange = false; @@ -527,7 +527,10 @@ class NoteEditorProvider with ChangeNotifier implements NoteEditorViewModel { // Get node ID from selection final nodeId = selection.extent.nodeId; - + if (nodeId == documentBuilder.titleNode.id) { + updateNoteTitle(noteId!, documentBuilder.titleNode.asTextNode.text.toPlainText()); + } + // Find the block ID for this node final blockId = _documentBuilder.nodeToBlockMap[nodeId]; if (blockId == null) return; diff --git a/src/frontend/lib/screens/note_editor_screen.dart b/src/frontend/lib/screens/note_editor_screen.dart index 2cdf484b..b5dae02b 100644 --- a/src/frontend/lib/screens/note_editor_screen.dart +++ b/src/frontend/lib/screens/note_editor_screen.dart @@ -108,6 +108,9 @@ class _NoteEditorScreenState extends State { // Load initial blocks for the note using ViewModel await _noteEditorViewModel.fetchBlocksForNote(_noteId!, page: 1, pageSize: 30); + + // Set the title in the editor + _noteEditorViewModel.documentBuilder.insertTitleNode(_note!.title); // Initialize the scroll listener for pagination - JUST ONCE _noteEditorViewModel.initScrollListener(_scrollController); @@ -197,16 +200,6 @@ class _NoteEditorScreenState extends State { } } - // Build the rich text editor directly, without using a separate widget - Widget _buildRichTextEditor(NoteEditorViewModel viewModel) { - // Remove redundant notification listener and use the scroll controller directly - return viewModel.documentBuilder.createSuperEditor( - readOnly: false, - scrollController: _scrollController, - themeData: Theme.of(context), - ); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -218,10 +211,7 @@ class _NoteEditorScreenState extends State { IconButton( icon: const Icon(Icons.refresh_outlined), tooltip: 'Refresh note content', - onPressed: () => _noteEditorViewModel.fetchBlocksForNote( - _noteEditorViewModel.noteId?? '', - refresh: true - ), + onPressed: () => _initialize(), ), const ThemeSwitcher(), ], @@ -236,7 +226,6 @@ class _NoteEditorScreenState extends State { if (noteEditorViewModel.currentNote != null && noteEditorViewModel.currentNote!.id == _noteId) { _note = noteEditorViewModel.currentNote; - // Update title if changed from server if (_note != null && _titleController.text != _note!.title && @@ -244,7 +233,7 @@ class _NoteEditorScreenState extends State { _titleController.text = _note!.title; } } - + // React to loading state final isContentLoading = noteEditorViewModel.isLoading; @@ -301,7 +290,10 @@ class _NoteEditorScreenState extends State { child: Stack( children: [ // Editor content - using direct integration - _buildRichTextEditor(noteEditorViewModel), + _noteEditorViewModel.documentBuilder.createSuperEditor( + scrollController: _scrollController, + themeData: Theme.of(context), + ), // Loading overlay if (isContentLoading) const Center(child: CircularProgressIndicator()), diff --git a/src/frontend/lib/utils/document_builder.dart b/src/frontend/lib/utils/document_builder.dart index e304804d..b72bbba3 100644 --- a/src/frontend/lib/utils/document_builder.dart +++ b/src/frontend/lib/utils/document_builder.dart @@ -23,6 +23,7 @@ class DocumentBuilder { late MutableDocumentComposer composer; late Editor editor; late FocusNode focusNode; + late DocumentNode titleNode; // Document layout key for accessing the document layout final GlobalKey documentLayoutKey = GlobalKey(); @@ -81,6 +82,15 @@ class DocumentBuilder { // Create focus node focusNode = FocusNode(); + titleNode = ParagraphNode( + id: Editor.createNodeId(), + text: AttributedText(""), + metadata: const { + 'blockType': NamedAttribution("header1"), + 'isDeletable': false + }, + ); + // Store initial node IDs for tracking structure changes _lastKnownNodeIds = document.map((node) => node.id).toList(); _lastKnownNodeCount = document.length; @@ -110,6 +120,19 @@ class DocumentBuilder { document.removeListener(listener); } + // Create title special node + void insertTitleNode(String title) { + titleNode = titleNode.asTextNode.copyTextNodeWith( + text: AttributedText(title), + ); + if (document.getNodeAt(0) != null && + !document.getNodeAt(0)!.isDeletable ) { + document.replaceNodeById(titleNode.id, titleNode); + } else { + document.insertNodeAt(0, titleNode); + } + } + // Insert a new node into the document void insertNode(DocumentNode node) { try { @@ -451,6 +474,12 @@ class DocumentBuilder { return false; } + // Skip if this node has it's note title + if (!node.isDeletable) { + _logger.debug('Node $nodeId is title node, skipping'); + return false; + } + // Mark as uncommitted _blockNodeMapping.markNodeAsUncommitted(nodeId); return true; @@ -1222,7 +1251,6 @@ class DocumentBuilder { // Create Super Editor with configured components for SuperEditor 0.3.0 Widget createSuperEditor({ - required bool readOnly, ScrollController? scrollController, ThemeData? themeData, }) { @@ -1244,7 +1272,6 @@ class DocumentBuilder { stylesheet: stylesheet, selectionStyle: selectionStyles, componentBuilders: componentBuilders, - keyboardActions: defaultKeyboardActions, documentOverlayBuilders: [ DefaultCaretOverlayBuilder( caretStyle: CaretStyle(