Files
nj/app/lib/data/models/journal_element.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,
},
);
}
}