// 编辑器 BLoC — 手写状态管理 + 元素管理 + 撤销/重做 + 自动保存 // // 状态机: // - 手写层:笔画列表 + 画笔设置 + 撤销/重做栈 // - 元素层:贴纸/照片/文字元素列表 + 选中元素 + 拖拽状态 // - 工具栏:当前活动工具 + 颜色面板 + 笔刷大小 // - 自动保存:笔画/元素变更 debounce → 触发保存回调 import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../widgets/stroke_model.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); } // --- 工具栏事件 --- /// 切换活动工具 class ToolChanged extends EditorEvent { final EditorTool tool; ToolChanged(this.tool); } /// 加载已有元素 class ElementsLoaded extends EditorEvent { final List elements; ElementsLoaded(this.elements); } // ============================================================ // 状态 // ============================================================ /// 编辑器工具枚举 enum EditorTool { pen, // 钢笔 pencil, // 铅笔 marker, // 马克笔 eraser, // 橡皮擦 select, // 选择/移动元素 text, // 文字输入 sticker, // 贴纸 image, // 照片 } /// 编辑器状态 class EditorState { // 手写层 final List strokes; final List redoStack; final BrushType brushType; final String brushColor; final double brushWidth; final int maxUndoSteps; // 元素层 final List elements; final String? selectedElementId; // 工具栏 final EditorTool activeTool; // 自动保存 final bool isDirty; final DateTime? lastSavedAt; const EditorState({ this.strokes = const [], this.redoStack = const [], this.brushType = BrushType.pen, this.brushColor = '#2D2420', this.brushWidth = 3.0, this.maxUndoSteps = 50, this.elements = const [], this.selectedElementId, this.activeTool = EditorTool.pen, this.isDirty = false, this.lastSavedAt, }); EditorState copyWith({ List? strokes, List? redoStack, BrushType? brushType, String? brushColor, double? brushWidth, List? elements, String? selectedElementId, bool clearSelection = false, EditorTool? activeTool, bool? isDirty, DateTime? lastSavedAt, }) => EditorState( strokes: strokes ?? this.strokes, redoStack: redoStack ?? this.redoStack, brushType: brushType ?? this.brushType, brushColor: brushColor ?? this.brushColor, brushWidth: brushWidth ?? this.brushWidth, maxUndoSteps: maxUndoSteps, elements: elements ?? this.elements, selectedElementId: clearSelection ? null : (selectedElementId ?? this.selectedElementId), activeTool: activeTool ?? this.activeTool, isDirty: isDirty ?? this.isDirty, lastSavedAt: lastSavedAt ?? this.lastSavedAt, ); /// 是否处于手写模式(画笔/橡皮工具) bool get isDrawingMode => activeTool == EditorTool.pen || activeTool == EditorTool.pencil || activeTool == EditorTool.marker || activeTool == EditorTool.eraser; /// 是否处于元素操作模式 bool get isElementMode => activeTool == EditorTool.select || activeTool == EditorTool.text || activeTool == EditorTool.sticker || activeTool == EditorTool.image; } // ============================================================ // 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(_onElementsLoaded); // 工具栏事件 on(_onToolChanged); } @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 _onElementsLoaded(ElementsLoaded event, Emitter emit) { emit(state.copyWith(elements: event.elements)); } // ============================================================ // 工具栏事件处理 // ============================================================ void _onToolChanged(ToolChanged event, Emitter emit) { // 切换工具时取消元素选中 emit(state.copyWith( activeTool: event.tool, clearSelection: true, )); } // ============================================================ // 自动保存 // ============================================================ /// 调度自动保存 — debounce 2 秒 void _scheduleAutoSave() { _saveTimer?.cancel(); _saveTimer = Timer(_saveDelay, () { if (state.isDirty && onSave != null) { onSave!(state.copyWith(isDirty: false, lastSavedAt: DateTime.now())); } }); } }