Flutter 手写引擎 (Phase F3): - stroke_model.dart: 笔画数据模型 (StrokePoint/Stroke/BrushType) - stroke_renderer.dart: perfect_freehand 渲染管线 + 四画笔参数 - handwriting_canvas.dart: Listener 输入 + 掌心抑制 + 去抖过滤 - editor_bloc.dart: BLoC 状态管理 + 撤销/重做 (50步) Rust 日记 CRUD + 同步 (Phase B2): - journal_service.rs: CRUD + 软删除 + 分页列表 + 事件发布 - sync_service.rs: 版本号同步 + 冲突检测 - journal_handler.rs: 5个API端点 + utoipa注解 + 权限守卫 - sync_handler.rs: 同步API端点 - error.rs: From<DiaryError> for AppError + 8个单元测试 - 路由注册: /diary/journals + /diary/sync 验证: - cargo check: 0 error - cargo test: 433 测试全通过 - flutter analyze: 1 warning (unused private param)
137 lines
3.5 KiB
Dart
137 lines
3.5 KiB
Dart
// 编辑器 BLoC — 手写状态管理 + 撤销/重做
|
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import '../widgets/stroke_model.dart';
|
|
|
|
// ===== Events =====
|
|
|
|
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);
|
|
}
|
|
|
|
// ===== State =====
|
|
|
|
class EditorState {
|
|
final List<Stroke> strokes;
|
|
final List<Stroke> redoStack;
|
|
final BrushType brushType;
|
|
final String brushColor;
|
|
final double brushWidth;
|
|
final int maxUndoSteps;
|
|
|
|
const EditorState({
|
|
this.strokes = const [],
|
|
this.redoStack = const [],
|
|
this.brushType = BrushType.pen,
|
|
this.brushColor = '#2D2420',
|
|
this.brushWidth = 3.0,
|
|
this.maxUndoSteps = 50,
|
|
});
|
|
|
|
EditorState copyWith({
|
|
List<Stroke>? strokes,
|
|
List<Stroke>? redoStack,
|
|
BrushType? brushType,
|
|
String? brushColor,
|
|
double? brushWidth,
|
|
}) =>
|
|
EditorState(
|
|
strokes: strokes ?? this.strokes,
|
|
redoStack: redoStack ?? this.redoStack,
|
|
brushType: brushType ?? this.brushType,
|
|
brushColor: brushColor ?? this.brushColor,
|
|
brushWidth: brushWidth ?? this.brushWidth,
|
|
maxUndoSteps: maxUndoSteps,
|
|
);
|
|
}
|
|
|
|
// ===== BLoC =====
|
|
|
|
class EditorBloc extends Bloc<EditorEvent, EditorState> {
|
|
EditorBloc() : super(const EditorState()) {
|
|
on<BrushChanged>(_onBrushChanged);
|
|
on<StrokeCompleted>(_onStrokeCompleted);
|
|
on<Undo>(_onUndo);
|
|
on<Redo>(_onRedo);
|
|
on<ClearCanvas>(_onClearCanvas);
|
|
on<StrokesLoaded>(_onStrokesLoaded);
|
|
}
|
|
|
|
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: [], // 新笔画清空重做栈
|
|
));
|
|
}
|
|
|
|
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,
|
|
));
|
|
}
|
|
|
|
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,
|
|
));
|
|
}
|
|
|
|
void _onClearCanvas(ClearCanvas event, Emitter<EditorState> emit) {
|
|
emit(state.copyWith(strokes: [], redoStack: []));
|
|
}
|
|
|
|
void _onStrokesLoaded(StrokesLoaded event, Emitter<EditorState> emit) {
|
|
emit(state.copyWith(strokes: event.strokes, redoStack: []));
|
|
}
|
|
}
|