219 lines
6.2 KiB
Dart
219 lines
6.2 KiB
Dart
// 日记元素数据模型 — 手写不可变类(避免 build_runner 依赖)
|
|
|
|
import 'dart:collection';
|
|
import 'dart:ui';
|
|
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
/// 元素类型 — 日记页面上的各种内容载体
|
|
enum ElementType {
|
|
text('text'),
|
|
image('image'),
|
|
sticker('sticker'),
|
|
handwritingRef('handwriting_ref'),
|
|
tape('tape');
|
|
|
|
const ElementType(this.value);
|
|
final String value;
|
|
}
|
|
|
|
/// 日记元素 — 日记页面中的单个内容单元
|
|
///
|
|
/// 每个元素有独立的位置、尺寸、旋转和层级。
|
|
/// [content] 字段根据 [elementType] 存储不同的结构化数据:
|
|
/// - text: {'text': '...', 'fontSize': 16.0, 'fontColor': '#2D2420'}
|
|
/// - image: {'filePath': '...', 'thumbnailPath': '...'}
|
|
/// - sticker: {'stickerPackId': '...', 'stickerId': '...'}
|
|
/// - handwriting_ref: {'strokesFileId': '...', 'strokeCount': 42}
|
|
/// - tape: {'tapeStyle': 'washi_dots', 'tapeColor': '#F2CC8F'}
|
|
class JournalElement {
|
|
final String id;
|
|
final String journalId;
|
|
final ElementType elementType;
|
|
final double positionX;
|
|
final double positionY;
|
|
final double width;
|
|
final double height;
|
|
final double rotation;
|
|
final int zIndex;
|
|
final Map<String, dynamic> content;
|
|
final int version;
|
|
final DateTime createdAt;
|
|
final DateTime updatedAt;
|
|
|
|
const JournalElement({
|
|
required this.id,
|
|
required this.journalId,
|
|
required this.elementType,
|
|
this.positionX = 0,
|
|
this.positionY = 0,
|
|
this.width = 100,
|
|
this.height = 100,
|
|
this.rotation = 0,
|
|
this.zIndex = 0,
|
|
this.content = const {},
|
|
this.version = 1,
|
|
required this.createdAt,
|
|
required this.updatedAt,
|
|
});
|
|
|
|
JournalElement copyWith({
|
|
String? id,
|
|
String? journalId,
|
|
ElementType? elementType,
|
|
double? positionX,
|
|
double? positionY,
|
|
double? width,
|
|
double? height,
|
|
double? rotation,
|
|
int? zIndex,
|
|
Map<String, dynamic>? content,
|
|
int? version,
|
|
DateTime? createdAt,
|
|
DateTime? updatedAt,
|
|
}) =>
|
|
JournalElement(
|
|
id: id ?? this.id,
|
|
journalId: journalId ?? this.journalId,
|
|
elementType: elementType ?? this.elementType,
|
|
positionX: positionX ?? this.positionX,
|
|
positionY: positionY ?? this.positionY,
|
|
width: width ?? this.width,
|
|
height: height ?? this.height,
|
|
rotation: rotation ?? this.rotation,
|
|
zIndex: zIndex ?? this.zIndex,
|
|
content: content ?? this.content,
|
|
version: version ?? this.version,
|
|
createdAt: createdAt ?? this.createdAt,
|
|
updatedAt: updatedAt ?? this.updatedAt,
|
|
);
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'id': id,
|
|
'journal_id': journalId,
|
|
'element_type': elementType.value,
|
|
'position_x': positionX,
|
|
'position_y': positionY,
|
|
'width': width,
|
|
'height': height,
|
|
'rotation': rotation,
|
|
'z_index': zIndex,
|
|
'content': content,
|
|
'version': version,
|
|
'created_at': createdAt.toIso8601String(),
|
|
'updated_at': updatedAt.toIso8601String(),
|
|
};
|
|
|
|
factory JournalElement.fromJson(Map<String, dynamic> json) => JournalElement(
|
|
id: json['id'] as String,
|
|
journalId: json['journal_id'] as String,
|
|
elementType: ElementType.values.firstWhere(
|
|
(e) => e.value == json['element_type'],
|
|
orElse: () => ElementType.text,
|
|
),
|
|
positionX: (json['position_x'] as num?)?.toDouble() ?? 0,
|
|
positionY: (json['position_y'] as num?)?.toDouble() ?? 0,
|
|
width: (json['width'] as num?)?.toDouble() ?? 100,
|
|
height: (json['height'] as num?)?.toDouble() ?? 100,
|
|
rotation: (json['rotation'] as num?)?.toDouble() ?? 0,
|
|
zIndex: (json['z_index'] as int?) ?? 0,
|
|
content: UnmodifiableMapView(
|
|
Map<String, dynamic>.from(json['content'] as Map? ?? {}),
|
|
),
|
|
version: (json['version'] as int?) ?? 1,
|
|
createdAt: DateTime.parse(json['created_at'] as String),
|
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
|
);
|
|
|
|
/// 创建新元素的工厂方法 — 自动生成 id 和时间戳
|
|
factory JournalElement.create({
|
|
required String journalId,
|
|
required ElementType elementType,
|
|
double positionX = 0,
|
|
double positionY = 0,
|
|
double width = 100,
|
|
double height = 100,
|
|
double rotation = 0,
|
|
int zIndex = 0,
|
|
Map<String, dynamic> content = const {},
|
|
}) {
|
|
final now = DateTime.now();
|
|
return JournalElement(
|
|
id: const Uuid().v4(),
|
|
journalId: journalId,
|
|
elementType: elementType,
|
|
positionX: positionX,
|
|
positionY: positionY,
|
|
width: width,
|
|
height: height,
|
|
rotation: rotation,
|
|
zIndex: zIndex,
|
|
content: content,
|
|
version: 1,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
);
|
|
}
|
|
|
|
/// 文字元素便利工厂
|
|
factory JournalElement.createText({
|
|
required String journalId,
|
|
required String text,
|
|
required Offset position,
|
|
double fontSize = 18.0,
|
|
String fontColor = '#2D2420',
|
|
}) {
|
|
return JournalElement.create(
|
|
journalId: journalId,
|
|
elementType: ElementType.text,
|
|
positionX: position.dx,
|
|
positionY: position.dy,
|
|
content: {
|
|
'text': text,
|
|
'fontSize': fontSize,
|
|
'fontColor': fontColor,
|
|
},
|
|
);
|
|
}
|
|
|
|
/// 图片元素便利工厂
|
|
factory JournalElement.createImage({
|
|
required String journalId,
|
|
required String filePath,
|
|
required Offset position,
|
|
String? thumbnailPath,
|
|
}) {
|
|
return JournalElement.create(
|
|
journalId: journalId,
|
|
elementType: ElementType.image,
|
|
positionX: position.dx,
|
|
positionY: position.dy,
|
|
content: {
|
|
'filePath': filePath,
|
|
if (thumbnailPath != null) 'thumbnailPath': thumbnailPath,
|
|
},
|
|
);
|
|
}
|
|
|
|
/// 贴纸元素便利工厂
|
|
factory JournalElement.createSticker({
|
|
required String journalId,
|
|
required String emoji,
|
|
required Offset position,
|
|
String? stickerPackId,
|
|
String? stickerId,
|
|
}) {
|
|
return JournalElement.create(
|
|
journalId: journalId,
|
|
elementType: ElementType.sticker,
|
|
positionX: position.dx,
|
|
positionY: position.dy,
|
|
content: {
|
|
'emoji': emoji,
|
|
if (stickerPackId != null) 'stickerPackId': stickerPackId,
|
|
if (stickerId != null) 'stickerId': stickerId,
|
|
},
|
|
);
|
|
}
|
|
}
|