// 编辑器 BLoC — 手写状态管理 + 元素管理 + 撤销/重做 + 自动保存 // // 状态机: // - 手写层:笔画列表 + 画笔设置 + 撤销/重做栈 // - 元素层:贴纸/照片/文字元素列表 + 选中元素 + 拖拽状态 // - 工具栏:当前活动工具 + 颜色面板 + 笔刷大小 // - 标签/心情:日记标签管理 + 心情选择 + 标题编辑 // - 自动保存:笔画/元素变更 debounce → 触发保存回调 import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../widgets/stroke_model.dart'; import '../../../data/models/journal_entry.dart'; import '../../../data/models/journal_element.dart'; // ============================================================ // 事件 // ============================================================ /// 编辑器事件基类 abstract class EditorEvent {} // --- 手写事件 --- class BrushChanged extends EditorEvent { final BrushType type; final String color; final double width; BrushChanged({required this.type, required this.color, required this.width}); } class StrokeCompleted extends EditorEvent { final Stroke stroke; StrokeCompleted(this.stroke); } class Undo extends EditorEvent {} class Redo extends EditorEvent {} class ClearCanvas extends EditorEvent {} class StrokesLoaded extends EditorEvent { final List strokes; StrokesLoaded(this.strokes); } // --- 元素事件 --- /// 添加元素(贴纸/照片/文字) class ElementAdded extends EditorEvent { final JournalElement element; ElementAdded(this.element); } /// 删除元素 class ElementRemoved extends EditorEvent { final String elementId; ElementRemoved(this.elementId); } /// 元素位置更新(拖拽中) class ElementMoved extends EditorEvent { final String elementId; final double positionX; final double positionY; ElementMoved({ required this.elementId, required this.positionX, required this.positionY, }); } /// 元素尺寸更新(缩放中) class ElementResized extends EditorEvent { final String elementId; final double width; final double height; ElementResized({ required this.elementId, required this.width, required this.height, }); } /// 元素旋转更新 class ElementRotated extends EditorEvent { final String elementId; final double rotation; ElementRotated({required this.elementId, required this.rotation}); } /// 选中/取消选中元素 class ElementSelected extends EditorEvent { final String? elementId; ElementSelected(this.elementId); } /// 图层顺序调整方向 enum LayerChange { bringToFront, sendToBack } /// 调整元素图层顺序 class ElementLayerChanged extends EditorEvent { final String elementId; final LayerChange change; ElementLayerChanged({required this.elementId, required this.change}); } // --- 工具栏事件 --- /// 切换活动工具 class ToolChanged extends EditorEvent { final EditorTool tool; ToolChanged(this.tool); } /// 再次点击已激活的工具 — 重新弹出设置面板 class ToolReactivated extends EditorEvent { final EditorTool tool; ToolReactivated(this.tool); } /// 加载已有元素 class ElementsLoaded extends EditorEvent { final List elements; ElementsLoaded(this.elements); } // --- 标签/心情/标题事件 --- /// 添加标签 class TagAdded extends EditorEvent { final String tag; TagAdded(this.tag); } /// 移除标签 class TagRemoved extends EditorEvent { final String tag; TagRemoved(this.tag); } /// 加载已有标签 class TagsLoaded extends EditorEvent { final List tags; TagsLoaded(this.tags); } /// 心情变更 class MoodChanged extends EditorEvent { final Mood mood; MoodChanged(this.mood); } /// 标题变更 class TitleChanged extends EditorEvent { final String title; TitleChanged(this.title); } /// 文字格式变更 class TextFormatChanged extends EditorEvent { final String elementId; final bool? bold; final bool? italic; final bool? underline; final String? color; final TextAlign? alignment; TextFormatChanged({ required this.elementId, this.bold, this.italic, this.underline, this.color, this.alignment, }); } /// 加载已有日记数据(从 JournalRepository 读取后原子注入) /// /// 与 StrokesLoaded/ElementsLoaded/TagsLoaded 等细粒度事件不同, /// LoadJournal 一次性还原所有日记状态,不触发 auto-save (isDirty=false)。 class LoadJournal extends EditorEvent { final String title; final Mood mood; final List tags; final List strokes; final List elements; final DateTime? lastSavedAt; LoadJournal({ required this.title, required this.mood, required this.tags, required this.strokes, required this.elements, this.lastSavedAt, }); } // ============================================================ // 状态 // ============================================================ /// 编辑器工具枚举 — 对应底部 6 个工具按钮 + 内部 select 模式 enum EditorTool { sticker, // 贴纸 template, // 模板 brush, // 画笔(含钢笔/铅笔/马克笔/橡皮子类型) photo, // 照片 text, // 文字 more, // 更多 select, // 选择/移动元素(内部模式,非 UI 按钮) } /// 编辑器状态 class EditorState { // 手写层 final List strokes; final List redoStack; final BrushType brushType; final String brushColor; final double brushWidth; final double brushOpacity; final int maxUndoSteps; // 元素层 final List elements; final String? selectedElementId; // 工具栏 final EditorTool activeTool; // 标签/心情/标题 final List tags; final Mood selectedMood; final String title; // 自动保存 final bool isDirty; final DateTime? lastSavedAt; // 工具重新激活时间戳(用于驱动面板重新弹出) final int toolReactivatedAt; const EditorState({ this.strokes = const [], this.redoStack = const [], this.brushType = BrushType.pen, this.brushColor = '#2D2420', this.brushWidth = 3.0, this.brushOpacity = 1.0, this.maxUndoSteps = 50, this.elements = const [], this.selectedElementId, this.activeTool = EditorTool.brush, this.tags = const [], this.selectedMood = Mood.calm, this.title = '', this.isDirty = false, this.lastSavedAt, this.toolReactivatedAt = 0, }); EditorState copyWith({ List? strokes, List? redoStack, BrushType? brushType, String? brushColor, double? brushWidth, double? brushOpacity, List? elements, String? selectedElementId, bool clearSelection = false, EditorTool? activeTool, List? tags, Mood? selectedMood, String? title, bool? isDirty, DateTime? lastSavedAt, int? toolReactivatedAt, }) => EditorState( strokes: strokes ?? this.strokes, redoStack: redoStack ?? this.redoStack, brushType: brushType ?? this.brushType, brushColor: brushColor ?? this.brushColor, brushWidth: brushWidth ?? this.brushWidth, brushOpacity: brushOpacity ?? this.brushOpacity, maxUndoSteps: maxUndoSteps, elements: elements ?? this.elements, selectedElementId: clearSelection ? null : (selectedElementId ?? this.selectedElementId), activeTool: activeTool ?? this.activeTool, tags: tags ?? this.tags, selectedMood: selectedMood ?? this.selectedMood, title: title ?? this.title, isDirty: isDirty ?? this.isDirty, lastSavedAt: lastSavedAt ?? this.lastSavedAt, toolReactivatedAt: toolReactivatedAt ?? this.toolReactivatedAt, ); /// 是否处于手写模式 bool get isDrawingMode => activeTool == EditorTool.brush; /// 是否处于元素操作模式 bool get isElementMode => activeTool == EditorTool.select || activeTool == EditorTool.text || activeTool == EditorTool.sticker || activeTool == EditorTool.photo; } // ============================================================ // BLoC // ============================================================ /// 编辑器 BLoC class EditorBloc extends Bloc { /// 自动保存回调 — 由上层(EditorPage)提供 final void Function(EditorState state)? onSave; /// 自动保存 debounce 定时器 Timer? _saveTimer; /// 自动保存延迟 static const _saveDelay = Duration(seconds: 2); EditorBloc({this.onSave}) : super(const EditorState()) { // 手写事件 on(_onBrushChanged); on(_onStrokeCompleted); on(_onUndo); on(_onRedo); on(_onClearCanvas); on(_onStrokesLoaded); // 元素事件 on(_onElementAdded); on(_onElementRemoved); on(_onElementMoved); on(_onElementResized); on(_onElementRotated); on(_onElementSelected); on(_onElementLayerChanged); on(_onElementsLoaded); // 日记加载事件 on(_onLoadJournal); // 工具栏事件 on(_onToolChanged); on(_onToolReactivated); // 标签/心情/标题事件 on(_onTagAdded); on(_onTagRemoved); on(_onTagsLoaded); on(_onMoodChanged); on(_onTitleChanged); on(_onTextFormatChanged); } @override Future close() { _saveTimer?.cancel(); return super.close(); } // ============================================================ // 手写事件处理 // ============================================================ void _onBrushChanged(BrushChanged event, Emitter emit) { emit(state.copyWith( brushType: event.type, brushColor: event.color, brushWidth: event.width, )); } void _onStrokeCompleted(StrokeCompleted event, Emitter emit) { final updatedStrokes = List.from(state.strokes)..add(event.stroke); if (updatedStrokes.length > state.maxUndoSteps) { updatedStrokes.removeAt(0); } emit(state.copyWith( strokes: updatedStrokes, redoStack: [], isDirty: true, )); _scheduleAutoSave(); } void _onUndo(Undo event, Emitter emit) { if (state.strokes.isEmpty) return; final updatedStrokes = List.from(state.strokes); final lastStroke = updatedStrokes.removeLast(); final updatedRedoStack = List.from(state.redoStack)..add(lastStroke); emit(state.copyWith( strokes: updatedStrokes, redoStack: updatedRedoStack, isDirty: true, )); _scheduleAutoSave(); } void _onRedo(Redo event, Emitter emit) { if (state.redoStack.isEmpty) return; final updatedRedoStack = List.from(state.redoStack); final stroke = updatedRedoStack.removeLast(); final updatedStrokes = List.from(state.strokes)..add(stroke); emit(state.copyWith( strokes: updatedStrokes, redoStack: updatedRedoStack, isDirty: true, )); _scheduleAutoSave(); } void _onClearCanvas(ClearCanvas event, Emitter emit) { emit(state.copyWith( strokes: [], redoStack: [], isDirty: true, )); _scheduleAutoSave(); } void _onStrokesLoaded(StrokesLoaded event, Emitter emit) { emit(state.copyWith(strokes: event.strokes, redoStack: [])); } // ============================================================ // 元素事件处理 // ============================================================ void _onElementAdded(ElementAdded event, Emitter emit) { final updated = List.from(state.elements)..add(event.element); emit(state.copyWith( elements: updated, selectedElementId: event.element.id, isDirty: true, )); _scheduleAutoSave(); } void _onElementRemoved(ElementRemoved event, Emitter emit) { final updated = List.from(state.elements) ..removeWhere((e) => e.id == event.elementId); emit(state.copyWith( elements: updated, clearSelection: state.selectedElementId == event.elementId, isDirty: true, )); _scheduleAutoSave(); } void _onElementMoved(ElementMoved event, Emitter emit) { final updated = state.elements.map((e) { if (e.id != event.elementId) return e; return e.copyWith( positionX: event.positionX, positionY: event.positionY, ); }).toList(); emit(state.copyWith(elements: updated, isDirty: true)); _scheduleAutoSave(); } void _onElementResized(ElementResized event, Emitter emit) { final updated = state.elements.map((e) { if (e.id != event.elementId) return e; return e.copyWith(width: event.width, height: event.height); }).toList(); emit(state.copyWith(elements: updated, isDirty: true)); _scheduleAutoSave(); } void _onElementRotated(ElementRotated event, Emitter emit) { final updated = state.elements.map((e) { if (e.id != event.elementId) return e; return e.copyWith(rotation: event.rotation); }).toList(); emit(state.copyWith(elements: updated, isDirty: true)); _scheduleAutoSave(); } void _onElementSelected(ElementSelected event, Emitter emit) { emit(state.copyWith( selectedElementId: event.elementId, clearSelection: event.elementId == null, )); } /// 调整元素图层顺序 — 置顶或置底 void _onElementLayerChanged( ElementLayerChanged event, Emitter emit, ) { final elements = List.from(state.elements); final index = elements.indexWhere((e) => e.id == event.elementId); if (index == -1) return; switch (event.change) { case LayerChange.bringToFront: // 设为最大 zIndex + 1 final maxZ = elements.fold( 0, (max, e) => e.zIndex > max ? e.zIndex : max, ); elements[index] = elements[index].copyWith(zIndex: maxZ + 1); case LayerChange.sendToBack: // 设为最小 zIndex - 1 final minZ = elements.fold( 0, (min, e) => e.zIndex < min ? e.zIndex : min, ); elements[index] = elements[index].copyWith(zIndex: minZ - 1); } emit(state.copyWith(elements: elements, isDirty: true)); _scheduleAutoSave(); } void _onElementsLoaded(ElementsLoaded event, Emitter emit) { emit(state.copyWith(elements: event.elements)); } // ============================================================ // 日记加载事件处理 // ============================================================ /// 加载已有日记 — 原子操作,一次性还原所有状态 /// /// 不触发 auto-save(isDirty=false),因为这是加载而非用户编辑。 void _onLoadJournal(LoadJournal event, Emitter emit) { emit(state.copyWith( title: event.title, selectedMood: event.mood, tags: event.tags, strokes: event.strokes, elements: event.elements, lastSavedAt: event.lastSavedAt, isDirty: false, )); } // ============================================================ // 工具栏事件处理 // ============================================================ void _onToolChanged(ToolChanged event, Emitter emit) { // 切换工具时取消元素选中 emit(state.copyWith( activeTool: event.tool, clearSelection: true, )); } void _onToolReactivated(ToolReactivated event, Emitter emit) { // 不改变 activeTool,仅递增时间戳驱动 UI 层重新弹出面板 emit(state.copyWith( toolReactivatedAt: DateTime.now().millisecondsSinceEpoch, )); } // ============================================================ // 标签/心情/标题事件处理 // ============================================================ void _onTagAdded(TagAdded event, Emitter emit) { if (state.tags.contains(event.tag)) return; if (state.tags.length >= 10) return; // 设计 Token: maxTags=10 final updated = List.from(state.tags)..add(event.tag); emit(state.copyWith(tags: updated, isDirty: true)); _scheduleAutoSave(); } void _onTagRemoved(TagRemoved event, Emitter emit) { final updated = List.from(state.tags)..remove(event.tag); emit(state.copyWith(tags: updated, isDirty: true)); _scheduleAutoSave(); } void _onTagsLoaded(TagsLoaded event, Emitter emit) { emit(state.copyWith(tags: event.tags)); } void _onMoodChanged(MoodChanged event, Emitter emit) { emit(state.copyWith(selectedMood: event.mood, isDirty: true)); _scheduleAutoSave(); } void _onTitleChanged(TitleChanged event, Emitter emit) { emit(state.copyWith(title: event.title, isDirty: true)); _scheduleAutoSave(); } void _onTextFormatChanged( TextFormatChanged event, Emitter emit, ) { final updated = state.elements.map((e) { if (e.id != event.elementId) return e; final content = Map.from(e.content); if (event.bold != null) content['bold'] = event.bold; if (event.italic != null) content['italic'] = event.italic; if (event.underline != null) content['underline'] = event.underline; if (event.color != null) content['color'] = event.color; if (event.alignment != null) { content['alignment'] = event.alignment!.index; } return e.copyWith(content: content); }).toList(); emit(state.copyWith(elements: updated, isDirty: true)); _scheduleAutoSave(); } // ============================================================ // 自动保存 // ============================================================ /// 调度自动保存 — debounce 2 秒 void _scheduleAutoSave() { _saveTimer?.cancel(); _saveTimer = Timer(_saveDelay, () { if (state.isDirty && onSave != null) { onSave!(state.copyWith(isDirty: false, lastSavedAt: DateTime.now())); } }); } }