Files
MookNote/lib/providers/note_plus_provider.dart
DelLevin-Home 9fd80f83c8 代码优化
2026-06-30 15:12:11 +08:00

460 lines
14 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<NotePlusDocument> _documents = [];
NotePlusDocument? _currentDocument;
List<NoteBlock> _blocks = [];
int _focusedBlockIndex = 0;
bool _isDirty = false;
// Undo/Redo
final List<List<NoteBlock>> _undoStack = [];
final List<List<NoteBlock>> _redoStack = [];
static const int _maxUndoSize = 50;
// Getters
List<NotePlusDocument> get documents => _documents;
NotePlusDocument? get currentDocument => _currentDocument;
List<NoteBlock> get blocks => _blocks;
int get focusedBlockIndex => _focusedBlockIndex;
bool get isDirty => _isDirty;
bool get canUndo => _undoStack.isNotEmpty;
bool get canRedo => _redoStack.isNotEmpty;
// ========== 文档 CRUD ==========
Future<void> loadDocuments() async {
_documents = await _dao.getAll();
notifyListeners();
}
Future<void> 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<void> 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<NotePlusDocument> createDocument({String title = '', String parentId = ''}) async {
final doc = NotePlusDocument(title: title, parentId: parentId);
await _dao.insert(doc);
await loadDocuments();
return doc;
}
/// 移动文档到指定父级的指定位置(原子操作,同时处理重排序)
Future<void> 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<void> 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<void> 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<void> restoreDocument(String id) async {
await _dao.restore(id);
await loadDocuments();
}
Future<void> permanentDeleteDocument(String id) async {
await _dao.permanentDelete(id);
await loadDocuments();
}
Future<List<NotePlusDocument>> getDeletedDocuments() async {
return await _dao.getDeleted();
}
Future<void> 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<void> 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<InlineFormatSpan>.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<InlineFormatType>.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<InlineFormatSpan> _clipFormatting(
List<InlineFormatSpan> 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<InlineFormatSpan> _shiftFormatting(
List<InlineFormatSpan> 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();
}
}