Files
nj/app/lib/features/editor/bloc/editor_bloc.dart
iven 482eb244d5 feat(app): 实现手账编辑器三层架构 (Phase F4)
新增组件:
- DraggableElement: 可拖拽日记元素组件 (移动/缩放/选中/删除)
- EditorToolbar: 底部工具栏 (8种工具 + 8色 + 5级笔宽 + 撤销/重做)
- EditorStack: 三层 Stack 架构 (Canvas + 元素 + 工具栏)

重写文件:
- editor_bloc.dart: 扩展为完整编辑器 BLoC
  - 元素管理: 添加/删除/移动/缩放/旋转/选中 (7种事件)
  - 工具栏: 8种工具切换 (pen/pencil/marker/eraser/select/text/sticker/image)
  - 自动保存: 2秒 debounce 回调
  - 状态扩展: elements/selectedElementId/activeTool/isDirty
- editor_page.dart: 从占位页面重写为完整编辑器
  - 顶栏: 返回/标题/完成按钮
  - 中间: 三层 Stack (手写层 + 元素层 + 空状态提示)
  - 底部: EditorToolbar
  - 交互逻辑: 画笔模式→Canvas接收, 选择模式→元素层接收

验证: flutter analyze (0 error)
2026-06-01 01:45:35 +08:00

411 lines
11 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.
// 编辑器 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<Stroke> 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<JournalElement> elements;
ElementsLoaded(this.elements);
}
// ============================================================
// 状态
// ============================================================
/// 编辑器工具枚举
enum EditorTool {
pen, // 钢笔
pencil, // 铅笔
marker, // 马克笔
eraser, // 橡皮擦
select, // 选择/移动元素
text, // 文字输入
sticker, // 贴纸
image, // 照片
}
/// 编辑器状态
class EditorState {
// 手写层
final List<Stroke> strokes;
final List<Stroke> redoStack;
final BrushType brushType;
final String brushColor;
final double brushWidth;
final int maxUndoSteps;
// 元素层
final List<JournalElement> 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<Stroke>? strokes,
List<Stroke>? redoStack,
BrushType? brushType,
String? brushColor,
double? brushWidth,
List<JournalElement>? 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<EditorEvent, EditorState> {
/// 自动保存回调 — 由上层EditorPage提供
final void Function(EditorState state)? onSave;
/// 自动保存 debounce 定时器
Timer? _saveTimer;
/// 自动保存延迟
static const _saveDelay = Duration(seconds: 2);
EditorBloc({this.onSave}) : super(const EditorState()) {
// 手写事件
on<BrushChanged>(_onBrushChanged);
on<StrokeCompleted>(_onStrokeCompleted);
on<Undo>(_onUndo);
on<Redo>(_onRedo);
on<ClearCanvas>(_onClearCanvas);
on<StrokesLoaded>(_onStrokesLoaded);
// 元素事件
on<ElementAdded>(_onElementAdded);
on<ElementRemoved>(_onElementRemoved);
on<ElementMoved>(_onElementMoved);
on<ElementResized>(_onElementResized);
on<ElementRotated>(_onElementRotated);
on<ElementSelected>(_onElementSelected);
on<ElementsLoaded>(_onElementsLoaded);
// 工具栏事件
on<ToolChanged>(_onToolChanged);
}
@override
Future<void> close() {
_saveTimer?.cancel();
return super.close();
}
// ============================================================
// 手写事件处理
// ============================================================
void _onBrushChanged(BrushChanged event, Emitter<EditorState> emit) {
emit(state.copyWith(
brushType: event.type,
brushColor: event.color,
brushWidth: event.width,
));
}
void _onStrokeCompleted(StrokeCompleted event, Emitter<EditorState> emit) {
final updatedStrokes = List<Stroke>.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<EditorState> emit) {
if (state.strokes.isEmpty) return;
final updatedStrokes = List<Stroke>.from(state.strokes);
final lastStroke = updatedStrokes.removeLast();
final updatedRedoStack = List<Stroke>.from(state.redoStack)..add(lastStroke);
emit(state.copyWith(
strokes: updatedStrokes,
redoStack: updatedRedoStack,
isDirty: true,
));
_scheduleAutoSave();
}
void _onRedo(Redo event, Emitter<EditorState> emit) {
if (state.redoStack.isEmpty) return;
final updatedRedoStack = List<Stroke>.from(state.redoStack);
final stroke = updatedRedoStack.removeLast();
final updatedStrokes = List<Stroke>.from(state.strokes)..add(stroke);
emit(state.copyWith(
strokes: updatedStrokes,
redoStack: updatedRedoStack,
isDirty: true,
));
_scheduleAutoSave();
}
void _onClearCanvas(ClearCanvas event, Emitter<EditorState> emit) {
emit(state.copyWith(
strokes: [],
redoStack: [],
isDirty: true,
));
_scheduleAutoSave();
}
void _onStrokesLoaded(StrokesLoaded event, Emitter<EditorState> emit) {
emit(state.copyWith(strokes: event.strokes, redoStack: []));
}
// ============================================================
// 元素事件处理
// ============================================================
void _onElementAdded(ElementAdded event, Emitter<EditorState> emit) {
final updated = List<JournalElement>.from(state.elements)..add(event.element);
emit(state.copyWith(
elements: updated,
selectedElementId: event.element.id,
isDirty: true,
));
_scheduleAutoSave();
}
void _onElementRemoved(ElementRemoved event, Emitter<EditorState> emit) {
final updated = List<JournalElement>.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<EditorState> 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<EditorState> 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<EditorState> 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<EditorState> emit) {
emit(state.copyWith(
selectedElementId: event.elementId,
clearSelection: event.elementId == null,
));
}
void _onElementsLoaded(ElementsLoaded event, Emitter<EditorState> emit) {
emit(state.copyWith(elements: event.elements));
}
// ============================================================
// 工具栏事件处理
// ============================================================
void _onToolChanged(ToolChanged event, Emitter<EditorState> 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()));
}
});
}
}