import 'package:flutter/foundation.dart'; import '../models/note_plus_models.dart'; import '../utils/note_plus/note_plus_dao.dart'; /// Note Plus 块编辑器状态管理 class NotePlusProvider extends ChangeNotifier { final NotePlusDao _dao = NotePlusDao(); List _documents = []; NotePlusDocument? _currentDocument; List _blocks = []; int _focusedBlockIndex = 0; bool _isDirty = false; // Undo/Redo final List> _undoStack = []; final List> _redoStack = []; static const int _maxUndoSize = 50; // Getters List get documents => _documents; NotePlusDocument? get currentDocument => _currentDocument; List get blocks => _blocks; int get focusedBlockIndex => _focusedBlockIndex; bool get isDirty => _isDirty; bool get canUndo => _undoStack.isNotEmpty; bool get canRedo => _redoStack.isNotEmpty; // ========== 文档 CRUD ========== Future loadDocuments() async { _documents = await _dao.getAll(); notifyListeners(); } Future loadDocumentById(String id) async { _currentDocument = await _dao.getById(id); if (_currentDocument != null) { _blocks = _currentDocument!.blocks.map((b) => b.deepCopy()).toList(); _focusedBlockIndex = 0; _isDirty = false; _undoStack.clear(); _redoStack.clear(); } notifyListeners(); } Future saveDocument({String? deltaJson}) async { if (_currentDocument == null) return; final now = DateTime.now(); // 直接构造,避免 copyWith 的 null 歧义 _currentDocument = NotePlusDocument( id: _currentDocument!.id, title: _currentDocument!.title, parentId: _currentDocument!.parentId, sortIndex: _currentDocument!.sortIndex, blocks: deltaJson == null ? List.from(_blocks) : _currentDocument!.blocks, blocksJson: deltaJson, tags: _currentDocument!.tags, images: _currentDocument!.images, createdAt: _currentDocument!.createdAt, updatedAt: now, isDeleted: _currentDocument!.isDeleted, ); await _dao.update(_currentDocument!); final idx = _documents.indexWhere((d) => d.id == _currentDocument!.id); if (idx >= 0) _documents[idx] = _currentDocument!; _isDirty = false; notifyListeners(); } /// 保存 Quill Delta JSON 作为文档内容(flutter_quill 模式) void saveDeltaJson(String deltaJson) { if (_currentDocument == null) return; _currentDocument = NotePlusDocument( id: _currentDocument!.id, title: _currentDocument!.title, parentId: _currentDocument!.parentId, sortIndex: _currentDocument!.sortIndex, blocksJson: deltaJson, tags: _currentDocument!.tags, images: _currentDocument!.images, createdAt: _currentDocument!.createdAt, updatedAt: DateTime.now(), isDeleted: _currentDocument!.isDeleted, ); _isDirty = true; } Future createDocument({String title = '', String parentId = ''}) async { final doc = NotePlusDocument(title: title, parentId: parentId); await _dao.insert(doc); await loadDocuments(); return doc; } /// 移动文档到指定父级的指定位置(原子操作,同时处理重排序) Future moveDocumentTo(String docId, String newParentId, int insertIndex) async { final idx = _documents.indexWhere((d) => d.id == docId); if (idx < 0) return; final doc = _documents[idx]; final oldParentId = doc.parentId; // 获取目标父级下的同级文档(不含自身) final siblings = _documents .where((d) => d.parentId == newParentId && d.id != docId) .toList() ..sort((a, b) => a.sortIndex.compareTo(b.sortIndex)); // 钳制插入位置 var pos = insertIndex.clamp(0, siblings.length); // 如果是同级移动,且拖到自己的后面,需要减 1(因为移除自己后索引会偏移) if (oldParentId == newParentId) { final oldIndex = siblings.indexWhere((d) => d.sortIndex > doc.sortIndex); if (oldIndex >= 0 && pos > oldIndex) { pos = (pos - 1).clamp(0, siblings.length); } } // 插入到指定位置 siblings.insert(pos, doc); // 批量更新 sortIndex 和 parentId for (int i = 0; i < siblings.length; i++) { final s = siblings[i]; final needsUpdate = s.sortIndex != i || s.id == docId && doc.parentId != newParentId; if (needsUpdate) { final updated = s.copyWith( parentId: newParentId, sortIndex: i, updatedAt: DateTime.now(), ); await _dao.update(updated); final uIdx = _documents.indexWhere((d) => d.id == updated.id); if (uIdx >= 0) _documents[uIdx] = updated; } } notifyListeners(); } /// 旧接口兼容:仅移动到新父级(放到末尾) Future moveDocument(String docId, String newParentId) async { final siblings = _documents.where((d) => d.parentId == newParentId && d.id != docId).toList(); await moveDocumentTo(docId, newParentId, siblings.length); } /// 删除文档时,将其子文档提升到祖父节点 Future deleteDocument(String id) async { final idx = _documents.indexWhere((d) => d.id == id); if (idx >= 0) { final parentId = _documents[idx].parentId; // 将子文档的 parent_id 改为被删文档的 parent_id final children = _documents.where((d) => d.parentId == id).toList(); for (final child in children) { final updated = child.copyWith(parentId: parentId, updatedAt: DateTime.now()); await _dao.update(updated); } } await _dao.delete(id); await loadDocuments(); } Future restoreDocument(String id) async { await _dao.restore(id); await loadDocuments(); } Future permanentDeleteDocument(String id) async { await _dao.permanentDelete(id); await loadDocuments(); } Future> getDeletedDocuments() async { return await _dao.getDeleted(); } Future searchDocuments(String query) async { _documents = await _dao.search(query); notifyListeners(); } // ========== 文档属性 ========== void setTitle(String title) { if (_currentDocument == null) return; _currentDocument = _currentDocument!.copyWith(title: title); _isDirty = true; notifyListeners(); } /// 重命名文档(在文档列表中调用) Future renameDocument(String id, String newTitle) async { final idx = _documents.indexWhere((d) => d.id == id); if (idx < 0) return; final doc = _documents[idx].copyWith(title: newTitle, updatedAt: DateTime.now()); await _dao.update(doc); _documents[idx] = doc; notifyListeners(); } // ========== Block 操作 ========== void setFocusedBlock(int index) { if (index < 0 || index >= _blocks.length) return; _focusedBlockIndex = index; notifyListeners(); } /// 添加 block(在指定位置之后) void addBlock(int afterIndex, NoteBlock block) { _pushUndo(); final insertAt = afterIndex + 1; if (insertAt >= _blocks.length) { _blocks.add(block); } else { _blocks.insert(insertAt, block); } _focusedBlockIndex = insertAt; _isDirty = true; notifyListeners(); } /// 移除 block void removeBlock(int index) { if (_blocks.length <= 1 || index < 0 || index >= _blocks.length) return; _pushUndo(); _blocks.removeAt(index); if (_focusedBlockIndex >= _blocks.length) { _focusedBlockIndex = _blocks.length - 1; } _isDirty = true; notifyListeners(); } /// 更新 block 内容(不触发 notify,编辑时高频调用) void updateBlockSilent(int index, NoteBlock block) { if (index < 0 || index >= _blocks.length) return; _blocks[index] = block; _isDirty = true; } /// 更新 block 并通知 void updateBlock(int index, NoteBlock block) { updateBlockSilent(index, block); notifyListeners(); } /// 拖拽移动 block void moveBlock(int oldIndex, int newIndex) { if (oldIndex == newIndex) return; _pushUndo(); final block = _blocks.removeAt(oldIndex); final insertAt = newIndex > oldIndex ? newIndex - 1 : newIndex; _blocks.insert(insertAt, block); _focusedBlockIndex = insertAt; _isDirty = true; notifyListeners(); } /// 转换 block 类型 void convertBlockType(int index, NoteBlockType newType) { if (index < 0 || index >= _blocks.length) return; _pushUndo(); _blocks[index] = _blocks[index].copyWith(type: newType); if (newType == NoteBlockType.divider) { _blocks[index] = _blocks[index].copyWith(text: ''); } _isDirty = true; notifyListeners(); } /// Enter 分割 block /// 返回新 block 的索引 int splitBlock(int index, int splitPosition) { if (index < 0 || index >= _blocks.length) return index; _pushUndo(); final current = _blocks[index]; final textBefore = current.text.substring(0, splitPosition); final textAfter = current.text.substring(splitPosition); // 裁剪当前 block 的格式 final beforeFormatting = _clipFormatting(current.formatting, 0, splitPosition); // 新 block 继承类型(heading 降为 paragraph) var newType = current.type; if (newType == NoteBlockType.heading1 || newType == NoteBlockType.heading2 || newType == NoteBlockType.heading3) { newType = NoteBlockType.paragraph; } // 平移新 block 的格式 final afterFormatting = _shiftFormatting(current.formatting, splitPosition, -splitPosition); // 更新当前 block _blocks[index] = current.copyWith( text: textBefore, formatting: beforeFormatting, ); // 插入新 block final newBlock = NoteBlock( type: newType, text: textAfter, formatting: afterFormatting, ); final insertAt = index + 1; _blocks.insert(insertAt, newBlock); _focusedBlockIndex = insertAt; _isDirty = true; notifyListeners(); return insertAt; } /// Backspace 合并到前一个 block /// 返回合并后的光标位置 int mergeWithPrevious(int index) { if (index <= 0 || index >= _blocks.length) return 0; _pushUndo(); final prev = _blocks[index - 1]; final current = _blocks[index]; final cursorPos = prev.text.length; // 合并文本 final mergedText = prev.text + current.text; // 合并格式:平移当前 block 的格式 final shiftedFormatting = _shiftFormatting(current.formatting, 0, cursorPos); final mergedFormatting = [...prev.formatting, ...shiftedFormatting]; // 更新前一个 block _blocks[index - 1] = prev.copyWith( text: mergedText, formatting: mergedFormatting, ); // 移除当前 block _blocks.removeAt(index); _focusedBlockIndex = index - 1; _isDirty = true; notifyListeners(); return cursorPos; } /// 切换待办状态 void toggleChecklist(int index) { if (index < 0 || index >= _blocks.length) return; final block = _blocks[index]; if (block.type != NoteBlockType.checklist) return; _pushUndo(); final checked = block.metadata['checked'] == true; _blocks[index] = block.copyWith( metadata: {...block.metadata, 'checked': !checked}, ); _isDirty = true; notifyListeners(); } /// 应用内联格式 void applyFormat(int blockIndex, InlineFormatType format, int start, int end) { if (blockIndex < 0 || blockIndex >= _blocks.length) return; if (start >= end) return; final block = _blocks[blockIndex]; final formatting = List.from(block.formatting); // 查找是否已有重叠的同格式 span bool found = false; for (int i = 0; i < formatting.length; i++) { final span = formatting[i]; if (span.start <= start && span.end >= end && span.formats.contains(format)) { // 移除格式 final newFormats = Set.from(span.formats)..remove(format); if (newFormats.isEmpty) { formatting.removeAt(i); } else { formatting[i] = span.copyWith(formats: newFormats); } found = true; break; } } if (!found) { formatting.add(InlineFormatSpan(start: start, end: end, formats: {format})); } _blocks[blockIndex] = block.copyWith(formatting: formatting); _isDirty = true; notifyListeners(); } // ========== Undo/Redo ========== void _pushUndo() { _undoStack.add(_blocks.map((b) => b.deepCopy()).toList()); if (_undoStack.length > _maxUndoSize) { _undoStack.removeAt(0); } _redoStack.clear(); } void undo() { if (_undoStack.isEmpty) return; _redoStack.add(_blocks.map((b) => b.deepCopy()).toList()); _blocks = _undoStack.removeLast(); if (_focusedBlockIndex >= _blocks.length) { _focusedBlockIndex = _blocks.length - 1; } _isDirty = true; notifyListeners(); } void redo() { if (_redoStack.isEmpty) return; _undoStack.add(_blocks.map((b) => b.deepCopy()).toList()); _blocks = _redoStack.removeLast(); if (_focusedBlockIndex >= _blocks.length) { _focusedBlockIndex = _blocks.length - 1; } _isDirty = true; notifyListeners(); } // ========== 格式辅助 ========== /// 裁剪格式区间到 [clipStart, clipEnd) List _clipFormatting( List spans, int clipStart, int clipEnd) { return spans .where((s) => s.end > clipStart && s.start < clipEnd) .map((s) => InlineFormatSpan( start: s.start < clipStart ? clipStart : s.start, end: s.end > clipEnd ? clipEnd : s.end, formats: Set.from(s.formats), )) .toList(); } /// 平移格式区间(从 fromPos 之后的 span 偏移 offset) List _shiftFormatting( List spans, int fromPos, int offset) { return spans .where((s) => s.end > fromPos) .map((s) => InlineFormatSpan( start: (s.start < fromPos ? fromPos : s.start) + offset, end: s.end + offset, formats: Set.from(s.formats), )) .where((s) => s.start >= 0 && s.end > s.start) .toList(); } }