// ============================================================ // 闲言APP — 精灵图动画贴纸数据模型 // 创建时间: 2026-04-29 // 更新时间: 2026-04-29 // 作用: 定义精灵图贴纸和表情包的数据结构 // 上次更新: 初始创建 // ============================================================ /// 精灵图贴纸来源 enum SpritesheetSource { builtin('builtin', '内置'), local('local', '本地导入'), remote('remote', '远程下载'); const SpritesheetSource(this.key, this.label); final String key; final String label; } /// 精灵图贴纸 — 单个动画贴纸 class SpritesheetSticker { const SpritesheetSticker({ required this.id, required this.name, required this.emoji, required this.source, required this.imageUri, required this.frameWidth, required this.frameHeight, required this.totalFrames, this.fps = 12, this.loop = true, this.packId, this.thumbnailUri, this.columns, this.rows, }); final String id; final String name; final String emoji; final SpritesheetSource source; final String imageUri; final int frameWidth; final int frameHeight; final int totalFrames; final int fps; final bool loop; final String? packId; final String? thumbnailUri; final int? columns; final int? rows; /// 推算列数:优先用 columns,否则假设单行排列 int get effectiveColumns => columns ?? totalFrames; /// 推算行数:优先用 rows,否则为1 int get effectiveRows => rows ?? 1; factory SpritesheetSticker.fromJson(Map json) { return SpritesheetSticker( id: json['id'] as String, name: json['name'] as String, emoji: json['emoji'] as String? ?? '🎭', source: SpritesheetSource.values.firstWhere( (s) => s.key == json['source'], orElse: () => SpritesheetSource.builtin, ), imageUri: json['imageUri'] as String, frameWidth: json['frameWidth'] as int, frameHeight: json['frameHeight'] as int, totalFrames: json['totalFrames'] as int, fps: json['fps'] as int? ?? 12, loop: json['loop'] as bool? ?? true, packId: json['packId'] as String?, thumbnailUri: json['thumbnailUri'] as String?, columns: json['columns'] as int?, rows: json['rows'] as int?, ); } Map toJson() => { 'id': id, 'name': name, 'emoji': emoji, 'source': source.key, 'imageUri': imageUri, 'frameWidth': frameWidth, 'frameHeight': frameHeight, 'totalFrames': totalFrames, 'fps': fps, 'loop': loop, 'packId': packId, 'thumbnailUri': thumbnailUri, 'columns': columns, 'rows': rows, }; SpritesheetSticker copyWith({ String? id, String? name, String? emoji, SpritesheetSource? source, String? imageUri, int? frameWidth, int? frameHeight, int? totalFrames, int? fps, bool? loop, String? packId, String? thumbnailUri, int? columns, int? rows, }) { return SpritesheetSticker( id: id ?? this.id, name: name ?? this.name, emoji: emoji ?? this.emoji, source: source ?? this.source, imageUri: imageUri ?? this.imageUri, frameWidth: frameWidth ?? this.frameWidth, frameHeight: frameHeight ?? this.frameHeight, totalFrames: totalFrames ?? this.totalFrames, fps: fps ?? this.fps, loop: loop ?? this.loop, packId: packId ?? this.packId, thumbnailUri: thumbnailUri ?? this.thumbnailUri, columns: columns ?? this.columns, rows: rows ?? this.rows, ); } } /// 表情包 — 一组精灵图贴纸 class StickerPack { const StickerPack({ required this.id, required this.name, required this.icon, required this.source, required this.stickers, this.coverUri, this.downloadUrl, this.fileSize, this.isDownloaded = false, }); final String id; final String name; final String icon; final SpritesheetSource source; final List stickers; final String? coverUri; final String? downloadUrl; final int? fileSize; final bool isDownloaded; factory StickerPack.fromJson(Map json) { return StickerPack( id: json['id'] as String, name: json['name'] as String, icon: json['icon'] as String? ?? '🎭', source: SpritesheetSource.values.firstWhere( (s) => s.key == json['source'], orElse: () => SpritesheetSource.builtin, ), stickers: (json['stickers'] as List?) ?.map( (s) => SpritesheetSticker.fromJson(s as Map), ) .toList() ?? [], coverUri: json['coverUri'] as String?, downloadUrl: json['downloadUrl'] as String?, fileSize: json['fileSize'] as int?, isDownloaded: json['isDownloaded'] as bool? ?? false, ); } Map toJson() => { 'id': id, 'name': name, 'icon': icon, 'source': source.key, 'stickers': stickers.map((s) => s.toJson()).toList(), 'coverUri': coverUri, 'downloadUrl': downloadUrl, 'fileSize': fileSize, 'isDownloaded': isDownloaded, }; StickerPack copyWith({ String? id, String? name, String? icon, SpritesheetSource? source, List? stickers, String? coverUri, String? downloadUrl, int? fileSize, bool? isDownloaded, }) { return StickerPack( id: id ?? this.id, name: name ?? this.name, icon: icon ?? this.icon, source: source ?? this.source, stickers: stickers ?? this.stickers, coverUri: coverUri ?? this.coverUri, downloadUrl: downloadUrl ?? this.downloadUrl, fileSize: fileSize ?? this.fileSize, isDownloaded: isDownloaded ?? this.isDownloaded, ); } }