Files
MookNote/lib/models/data_models.dart
2026-08-09 01:22:35 +08:00

1686 lines
49 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.
import 'dart:io';
import 'dart:convert';
/// 用于区分 copyWith 中"未传参数"和"传了 null"的标记
class _CopyWithNullSentinel {
const _CopyWithNullSentinel();
}
const _copyWithNull = _CopyWithNullSentinel();
/// 安全解析日期字符串,失败时返回 fallback
DateTime? _safeParseDate(String? str, {DateTime? fallback}) {
if (str == null || str.isEmpty) return fallback;
return DateTime.tryParse(str)?.toLocal() ?? fallback;
}
/// 安全将动态值转为 double失败时返回 fallback
double? _safeParseDouble(dynamic value, {double? fallback}) {
if (value == null) return fallback;
if (value is double) return value;
if (value is int) return value.toDouble();
if (value is num) return value.toDouble();
if (value is String) return double.tryParse(value) ?? fallback;
return fallback;
}
/// 解析字符串列表(通用工具函数,不限于 Movie
List<String> parseStringListGeneric(dynamic data) {
if (data == null) return [];
if (data is List) {
return data.map((e) => e.toString()).toList();
}
if (data is String) {
if (data.isEmpty) return [];
try {
final decoded = jsonDecode(data);
if (decoded is List) {
return decoded.map((e) => e.toString()).toList();
}
// JSON 解析成功但不是 List如 Map、String保留原始值
return [data];
} catch (e) {
// JSON 解析失败,按逗号分割;若无逗号则作为单元素保留
final split = data.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty).toList();
return split.isNotEmpty ? split : [data];
}
}
return [];
}
/// 影视条目模型
class Movie {
final String id;
final String title; // 影视名称
final String? posterPath; // 本地海报路径
final DateTime? releaseDate; // 上映时间
final List<String> directors; // 导演列表
final List<String> writers; // 编剧列表
final List<String> actors; // 主演列表
final List<String> genres; // 类型
final List<String> alternateTitles; // 别名
final String? summary; // 剧情简介
final double? rating; // 评分 1-10
final String status; // watched/want_to_watch/watching
final String category; // 影视分类: movie/tv/anime/variety/documentary/short/other
final DateTime? watchDate; // 观看日期
final int watchCount; // 观看次数
final DateTime createdAt;
final DateTime updatedAt;
final bool isDeleted;
final double coverOffset; // 封面偏移量
Movie({
required this.id,
required this.title,
this.posterPath,
this.releaseDate,
this.directors = const [],
this.writers = const [],
this.actors = const [],
this.genres = const [],
this.alternateTitles = const [],
this.summary,
this.rating,
required this.status,
this.category = 'movie',
this.watchDate,
this.watchCount = 0,
required this.createdAt,
required this.updatedAt,
this.isDeleted = false,
this.coverOffset = 0.0,
});
factory Movie.fromJson(Map<String, dynamic> json) {
return Movie(
id: json['id'] ?? '',
title: json['title'] ?? '',
posterPath: json['poster_path'],
releaseDate: _safeParseDate(json['release_date']),
directors: _parseStringList(json['directors']),
writers: _parseStringList(json['writers']),
actors: _parseStringList(json['actors']),
genres: _parseStringList(json['genres']),
alternateTitles: _parseStringList(json['alternate_titles']),
summary: json['summary'],
rating: _safeParseDouble(json['rating']),
status: json['status'] ?? 'want_to_watch',
category: json['category'] ?? 'movie',
watchDate: _safeParseDate(json['watch_date']),
watchCount: json['watch_count'] ?? 0,
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
updatedAt: _safeParseDate(json['updated_at'], fallback: DateTime.now())!,
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
coverOffset: _safeParseDouble(json['cover_offset'], fallback: 0.0)!,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'poster_path': posterPath,
'release_date': releaseDate?.toUtc().toIso8601String(),
'directors': jsonEncode(directors),
'writers': jsonEncode(writers),
'actors': jsonEncode(actors),
'genres': jsonEncode(genres),
'alternate_titles': jsonEncode(alternateTitles),
'summary': summary,
'rating': rating,
'status': status,
'category': category,
'watch_date': watchDate?.toUtc().toIso8601String(),
'watch_count': watchCount,
'created_at': createdAt.toUtc().toIso8601String(),
'updated_at': updatedAt.toUtc().toIso8601String(),
'is_deleted': isDeleted ? 1 : 0,
'cover_offset': coverOffset,
};
}
/// 获取封面文件
File? get posterFile {
if (posterPath == null || posterPath!.isEmpty) return null;
return File(posterPath!);
}
/// 解析字符串列表(公共静态方法,供外部使用)
static List<String> parseStringList(dynamic data) => parseStringListGeneric(data);
/// 解析字符串列表(私有别名,保持兼容性)
static List<String> _parseStringList(dynamic data) => parseStringListGeneric(data);
/// 复制并修改
Movie copyWith({
String? id,
String? title,
Object? posterPath = _copyWithNull,
DateTime? releaseDate,
List<String>? directors,
List<String>? writers,
List<String>? actors,
List<String>? genres,
List<String>? alternateTitles,
Object? summary = _copyWithNull,
Object? rating = _copyWithNull,
String? status,
String? category,
DateTime? watchDate,
int? watchCount,
DateTime? createdAt,
DateTime? updatedAt,
bool? isDeleted,
double? coverOffset,
}) {
return Movie(
id: id ?? this.id,
title: title ?? this.title,
posterPath: posterPath is _CopyWithNullSentinel ? this.posterPath : (posterPath as String?),
releaseDate: releaseDate ?? this.releaseDate,
directors: directors ?? this.directors,
writers: writers ?? this.writers,
actors: actors ?? this.actors,
genres: genres ?? this.genres,
alternateTitles: alternateTitles ?? this.alternateTitles,
summary: summary is _CopyWithNullSentinel ? this.summary : (summary as String?),
rating: rating is _CopyWithNullSentinel ? this.rating : (rating as double?),
status: status ?? this.status,
category: category ?? this.category,
watchDate: watchDate ?? this.watchDate,
watchCount: watchCount ?? this.watchCount,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
isDeleted: isDeleted ?? this.isDeleted,
coverOffset: coverOffset ?? this.coverOffset,
);
}
}
/// 书籍条目模型
class Book {
final String id;
final String title; // 书籍名称
final String? coverPath; // 本地封面路径
final List<String> authors; // 作者列表
final List<String> translators; // 译者列表
final List<String> alternateTitles; // 别名
final String? publisher; // 出版社
final List<String> genres; // 类型
final String? summary; // 书籍简介
final double? rating; // 评分 1-10
final String status; // read/reading/want_to_read
final String? isbn; // ISBN编号
final DateTime? publishDate; // 出版时间
final DateTime? startDate; // 开始阅读日期
final DateTime? finishDate; // 读完日期
final int readCount; // 阅读次数
final DateTime createdAt;
final DateTime updatedAt;
final bool isDeleted;
final double coverOffset; // 封面偏移量
Book({
required this.id,
required this.title,
this.coverPath,
this.authors = const [],
this.translators = const [],
this.alternateTitles = const [],
this.publisher,
this.genres = const [],
this.summary,
this.rating,
required this.status,
this.isbn,
this.publishDate,
this.startDate,
this.finishDate,
this.readCount = 0,
required this.createdAt,
required this.updatedAt,
this.isDeleted = false,
this.coverOffset = 0.0,
});
factory Book.fromJson(Map<String, dynamic> json) {
return Book(
id: json['id'] ?? '',
title: json['title'] ?? '',
coverPath: json['cover_path'],
authors: Movie.parseStringList(json['authors']),
translators: Movie.parseStringList(json['translators']),
alternateTitles: Movie.parseStringList(json['alternate_titles']),
publisher: json['publisher'],
genres: Movie.parseStringList(json['genres']),
summary: json['summary'],
rating: _safeParseDouble(json['rating']),
status: json['status'] ?? 'want_to_read',
isbn: json['isbn'],
publishDate: _safeParseDate(json['publish_date']),
startDate: _safeParseDate(json['start_date']),
finishDate: _safeParseDate(json['finish_date']),
readCount: json['read_count'] ?? 0,
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
updatedAt: _safeParseDate(json['updated_at'], fallback: DateTime.now())!,
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
coverOffset: _safeParseDouble(json['cover_offset'], fallback: 0.0)!,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'cover_path': coverPath,
'authors': jsonEncode(authors),
'translators': jsonEncode(translators),
'alternate_titles': jsonEncode(alternateTitles),
'publisher': publisher,
'genres': jsonEncode(genres),
'summary': summary,
'rating': rating,
'status': status,
'isbn': isbn,
'publish_date': publishDate?.toUtc().toIso8601String(),
'start_date': startDate?.toUtc().toIso8601String(),
'finish_date': finishDate?.toUtc().toIso8601String(),
'read_count': readCount,
'created_at': createdAt.toUtc().toIso8601String(),
'updated_at': updatedAt.toUtc().toIso8601String(),
'is_deleted': isDeleted ? 1 : 0,
'cover_offset': coverOffset,
};
}
/// 获取封面文件
File? get coverFile {
if (coverPath == null || coverPath!.isEmpty) return null;
return File(coverPath!);
}
/// 复制并修改
Book copyWith({
String? id,
String? title,
Object? coverPath = _copyWithNull,
List<String>? authors,
List<String>? translators,
List<String>? alternateTitles,
String? publisher,
List<String>? genres,
Object? summary = _copyWithNull,
Object? rating = _copyWithNull,
String? status,
Object? isbn = _copyWithNull,
DateTime? publishDate,
DateTime? startDate,
DateTime? finishDate,
int? readCount,
DateTime? createdAt,
DateTime? updatedAt,
bool? isDeleted,
double? coverOffset,
}) {
return Book(
id: id ?? this.id,
title: title ?? this.title,
coverPath: coverPath is _CopyWithNullSentinel ? this.coverPath : (coverPath as String?),
authors: authors ?? this.authors,
translators: translators ?? this.translators,
alternateTitles: alternateTitles ?? this.alternateTitles,
publisher: publisher ?? this.publisher,
genres: genres ?? this.genres,
summary: summary is _CopyWithNullSentinel ? this.summary : (summary as String?),
rating: rating is _CopyWithNullSentinel ? this.rating : (rating as double?),
status: status ?? this.status,
isbn: isbn is _CopyWithNullSentinel ? this.isbn : (isbn as String?),
publishDate: publishDate ?? this.publishDate,
startDate: startDate ?? this.startDate,
finishDate: finishDate ?? this.finishDate,
readCount: readCount ?? this.readCount,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
isDeleted: isDeleted ?? this.isDeleted,
coverOffset: coverOffset ?? this.coverOffset,
);
}
}
/// 笔记模型
class Note {
final String id;
final String title;
final String content;
final String contentType; // 内容类型markdown
final List<String> tags;
final List<String> images; // 图片路径列表
final DateTime createdAt;
final DateTime updatedAt;
final bool isDeleted;
final bool isPinned;
Note({
required this.id,
required this.title,
required this.content,
this.contentType = 'markdown',
this.tags = const [],
this.images = const [],
required this.createdAt,
required this.updatedAt,
this.isDeleted = false,
this.isPinned = false,
});
factory Note.fromJson(Map<String, dynamic> json) {
return Note(
id: json['id']?.toString() ?? '',
title: json['title'] ?? '',
content: json['content'] ?? '',
contentType: json['content_type'] ?? 'markdown',
tags: Movie.parseStringList(json['tags']),
images: Movie.parseStringList(json['images']),
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
updatedAt: _safeParseDate(json['updated_at'], fallback: DateTime.now())!,
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
isPinned: json['is_pinned'] == 1 || json['is_pinned'] == true,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'content': content,
'content_type': contentType,
'tags': jsonEncode(tags),
'images': jsonEncode(images),
'created_at': createdAt.toUtc().toIso8601String(),
'updated_at': updatedAt.toUtc().toIso8601String(),
'is_deleted': isDeleted ? 1 : 0,
'is_pinned': isPinned ? 1 : 0,
};
}
/// 复制并修改
Note copyWith({
String? id,
String? title,
String? content,
String? contentType,
List<String>? tags,
List<String>? images,
DateTime? createdAt,
DateTime? updatedAt,
bool? isDeleted,
bool? isPinned,
}) {
return Note(
id: id ?? this.id,
title: title ?? this.title,
content: content ?? this.content,
contentType: contentType ?? this.contentType,
tags: tags ?? this.tags,
images: images ?? this.images,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
isDeleted: isDeleted ?? this.isDeleted,
isPinned: isPinned ?? this.isPinned,
);
}
/// 获取内容摘要前100字
String get summary {
if (content.length <= 100) return content;
return '${content.substring(0, 100)}...';
}
}
/// 影评模型
class MovieReview {
final String id;
final String movieId;
final String content;
final String reviewer;
final String source;
final int reviewType; // 1: 短评, 2: 长评
final bool isDeleted;
final DateTime createdAt;
final DateTime updatedAt;
MovieReview({
required this.id,
required this.movieId,
required this.content,
this.reviewer = '',
this.source = '',
this.reviewType = 1,
this.isDeleted = false,
required this.createdAt,
required this.updatedAt,
});
factory MovieReview.fromJson(Map<String, dynamic> json) {
return MovieReview(
id: json['id']?.toString() ?? '',
movieId: json['movie_id']?.toString() ?? '',
content: json['content'] ?? '',
reviewer: json['reviewer'] ?? '',
source: json['source'] ?? '',
reviewType: json['review_type'] ?? 1,
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
updatedAt: _safeParseDate(json['updated_at'], fallback: DateTime.now())!,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'movie_id': movieId,
'content': content,
'reviewer': reviewer,
'source': source,
'review_type': reviewType,
'is_deleted': isDeleted ? 1 : 0,
'created_at': createdAt.toUtc().toIso8601String(),
'updated_at': updatedAt.toUtc().toIso8601String(),
};
}
/// 复制并修改
MovieReview copyWith({
String? id,
String? movieId,
String? content,
String? reviewer,
String? source,
int? reviewType,
bool? isDeleted,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return MovieReview(
id: id ?? this.id,
movieId: movieId ?? this.movieId,
content: content ?? this.content,
reviewer: reviewer ?? this.reviewer,
source: source ?? this.source,
reviewType: reviewType ?? this.reviewType,
isDeleted: isDeleted ?? this.isDeleted,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
/// 获取评论摘要
String get summary {
if (content.length <= 50) return content;
return '${content.substring(0, 50)}...';
}
/// 评论类型文本
String get typeText => reviewType == 1 ? '短评' : '长评';
}
/// 影视海报墙模型
class MoviePoster {
final String id;
final String movieId;
final String posterPath;
final bool isDeleted;
final DateTime createdAt;
MoviePoster({
required this.id,
required this.movieId,
required this.posterPath,
this.isDeleted = false,
required this.createdAt,
});
factory MoviePoster.fromJson(Map<String, dynamic> json) {
return MoviePoster(
id: json['id']?.toString() ?? '',
movieId: json['movie_id']?.toString() ?? '',
posterPath: json['poster_path'] ?? '',
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'movie_id': movieId,
'poster_path': posterPath,
'is_deleted': isDeleted ? 1 : 0,
'created_at': createdAt.toUtc().toIso8601String(),
};
}
/// 获取海报文件
File? get posterFile {
if (posterPath.isEmpty) return null;
return File(posterPath);
}
}
/// 书评模型
class BookReview {
final String id;
final String bookId;
final String content;
final String reviewer;
final String source;
final int reviewType; // 1: 短评, 2: 长评
final bool isDeleted;
final DateTime createdAt;
final DateTime updatedAt;
BookReview({
required this.id,
required this.bookId,
required this.content,
this.reviewer = '',
this.source = '',
this.reviewType = 1,
this.isDeleted = false,
required this.createdAt,
required this.updatedAt,
});
factory BookReview.fromJson(Map<String, dynamic> json) {
return BookReview(
id: json['id']?.toString() ?? '',
bookId: json['book_id']?.toString() ?? '',
content: json['content'] ?? '',
reviewer: json['reviewer'] ?? '',
source: json['source'] ?? '',
reviewType: json['review_type'] ?? 1,
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
updatedAt: _safeParseDate(json['updated_at'], fallback: DateTime.now())!,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'book_id': bookId,
'content': content,
'reviewer': reviewer,
'source': source,
'review_type': reviewType,
'is_deleted': isDeleted ? 1 : 0,
'created_at': createdAt.toUtc().toIso8601String(),
'updated_at': updatedAt.toUtc().toIso8601String(),
};
}
/// 复制并修改
BookReview copyWith({
String? id,
String? bookId,
String? content,
String? reviewer,
String? source,
int? reviewType,
bool? isDeleted,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return BookReview(
id: id ?? this.id,
bookId: bookId ?? this.bookId,
content: content ?? this.content,
reviewer: reviewer ?? this.reviewer,
source: source ?? this.source,
reviewType: reviewType ?? this.reviewType,
isDeleted: isDeleted ?? this.isDeleted,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
/// 获取评论摘要
String get summary {
if (content.length <= 50) return content;
return '${content.substring(0, 50)}...';
}
/// 评论类型文本
String get typeText => reviewType == 1 ? '短评' : '长评';
}
/// 游戏条目模型
class Game {
final String id;
final String title; // 游戏名称
final String? coverPath; // 本地封面路径
final double? rating; // 评分 1-10
final String status; // completed/playing/want_to_play/abandoned
final String category; // 游戏分类: digital/cartridge/disc
final List<String> platforms; // 平台列表
final List<String> versions; // 版本列表
final List<String> genres; // 类型
final int playTimeHours; // 游玩时长(小时)
final int playTimeMinutes; // 游玩时长(分钟)
final int playCount; // 游玩次数
final List<String> developer; // 开发者
final DateTime? releaseDate; // 发售时间
final List<String> purchasePlatforms; // 购买平台
final DateTime? purchaseDate; // 购买日期
final String? purchasePrice; // 购买价格
final String? summary; // 游戏简介
final double coverOffset; // 封面偏移量
final DateTime createdAt;
final DateTime updatedAt;
final bool isDeleted;
Game({
required this.id,
required this.title,
this.coverPath,
this.rating,
required this.status,
this.category = 'digital',
this.platforms = const [],
this.versions = const [],
this.genres = const [],
this.playTimeHours = 0,
this.playTimeMinutes = 0,
this.playCount = 0,
this.developer = const [],
this.releaseDate,
this.purchasePlatforms = const [],
this.purchaseDate,
this.purchasePrice,
this.summary,
this.coverOffset = 0.0,
required this.createdAt,
required this.updatedAt,
this.isDeleted = false,
});
factory Game.fromJson(Map<String, dynamic> json) {
return Game(
id: json['id'] ?? '',
title: json['title'] ?? '',
coverPath: json['cover_path'],
rating: _safeParseDouble(json['rating']),
status: json['status'] ?? 'want_to_play',
category: json['category'] ?? 'digital',
platforms: parseStringListGeneric(json['platforms']),
versions: parseStringListGeneric(json['versions']),
genres: parseStringListGeneric(json['genres']),
playTimeHours: json['play_time_hours'] ?? 0,
playTimeMinutes: json['play_time_minutes'] ?? 0,
playCount: json['play_count'] ?? 0,
developer: parseStringListGeneric(json['developer']),
releaseDate: _safeParseDate(json['release_date']),
purchasePlatforms: parseStringListGeneric(json['purchase_platforms']),
purchaseDate: _safeParseDate(json['purchase_date']),
purchasePrice: json['purchase_price'],
summary: json['summary'],
coverOffset: _safeParseDouble(json['cover_offset'], fallback: 0.0)!,
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
updatedAt: _safeParseDate(json['updated_at'], fallback: DateTime.now())!,
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'cover_path': coverPath,
'rating': rating,
'status': status,
'category': category,
'platforms': jsonEncode(platforms),
'versions': jsonEncode(versions),
'genres': jsonEncode(genres),
'play_time_hours': playTimeHours,
'play_time_minutes': playTimeMinutes,
'play_count': playCount,
'developer': jsonEncode(developer),
'release_date': releaseDate?.toUtc().toIso8601String(),
'purchase_platforms': jsonEncode(purchasePlatforms),
'purchase_date': purchaseDate?.toUtc().toIso8601String(),
'purchase_price': purchasePrice,
'summary': summary,
'cover_offset': coverOffset,
'created_at': createdAt.toUtc().toIso8601String(),
'updated_at': updatedAt.toUtc().toIso8601String(),
'is_deleted': isDeleted ? 1 : 0,
};
}
/// 获取封面文件
File? get coverFile {
if (coverPath == null || coverPath!.isEmpty) return null;
return File(coverPath!);
}
/// 复制并修改
Game copyWith({
String? id,
String? title,
Object? coverPath = _copyWithNull,
Object? rating = _copyWithNull,
String? status,
String? category,
List<String>? platforms,
List<String>? versions,
List<String>? genres,
int? playTimeHours,
int? playTimeMinutes,
int? playCount,
List<String>? developer,
DateTime? releaseDate,
List<String>? purchasePlatforms,
DateTime? purchaseDate,
Object? purchasePrice = _copyWithNull,
Object? summary = _copyWithNull,
double? coverOffset,
DateTime? createdAt,
DateTime? updatedAt,
bool? isDeleted,
}) {
return Game(
id: id ?? this.id,
title: title ?? this.title,
coverPath: coverPath is _CopyWithNullSentinel ? this.coverPath : (coverPath as String?),
rating: rating is _CopyWithNullSentinel ? this.rating : (rating as double?),
status: status ?? this.status,
category: category ?? this.category,
platforms: platforms ?? this.platforms,
versions: versions ?? this.versions,
genres: genres ?? this.genres,
playTimeHours: playTimeHours ?? this.playTimeHours,
playTimeMinutes: playTimeMinutes ?? this.playTimeMinutes,
playCount: playCount ?? this.playCount,
developer: developer ?? this.developer,
releaseDate: releaseDate ?? this.releaseDate,
purchasePlatforms: purchasePlatforms ?? this.purchasePlatforms,
purchaseDate: purchaseDate ?? this.purchaseDate,
purchasePrice: purchasePrice is _CopyWithNullSentinel ? this.purchasePrice : (purchasePrice as String?),
summary: summary is _CopyWithNullSentinel ? this.summary : (summary as String?),
coverOffset: coverOffset ?? this.coverOffset,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
isDeleted: isDeleted ?? this.isDeleted,
);
}
}
/// 游戏评价模型
class GameReview {
final String id;
final String gameId;
final String content;
final String reviewer;
final String source;
final int reviewType; // 1: 短评, 2: 长评
final bool isDeleted;
final DateTime createdAt;
final DateTime updatedAt;
GameReview({
required this.id,
required this.gameId,
required this.content,
this.reviewer = '',
this.source = '',
this.reviewType = 1,
this.isDeleted = false,
required this.createdAt,
required this.updatedAt,
});
factory GameReview.fromJson(Map<String, dynamic> json) {
return GameReview(
id: json['id']?.toString() ?? '',
gameId: json['game_id']?.toString() ?? '',
content: json['content'] ?? '',
reviewer: json['reviewer'] ?? '',
source: json['source'] ?? '',
reviewType: json['review_type'] ?? 1,
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
updatedAt: _safeParseDate(json['updated_at'], fallback: DateTime.now())!,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'game_id': gameId,
'content': content,
'reviewer': reviewer,
'source': source,
'review_type': reviewType,
'is_deleted': isDeleted ? 1 : 0,
'created_at': createdAt.toUtc().toIso8601String(),
'updated_at': updatedAt.toUtc().toIso8601String(),
};
}
GameReview copyWith({
String? id,
String? gameId,
String? content,
String? reviewer,
String? source,
int? reviewType,
bool? isDeleted,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return GameReview(
id: id ?? this.id,
gameId: gameId ?? this.gameId,
content: content ?? this.content,
reviewer: reviewer ?? this.reviewer,
source: source ?? this.source,
reviewType: reviewType ?? this.reviewType,
isDeleted: isDeleted ?? this.isDeleted,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
String get summary => content.length <= 50 ? content : '${content.substring(0, 50)}...';
String get typeText => reviewType == 1 ? '短评' : '长评';
}
/// 游戏截图模型
class GameScreenshot {
final String id;
final String gameId;
final String screenshotPath;
final bool isDeleted;
final DateTime createdAt;
GameScreenshot({
required this.id,
required this.gameId,
required this.screenshotPath,
this.isDeleted = false,
required this.createdAt,
});
factory GameScreenshot.fromJson(Map<String, dynamic> json) {
return GameScreenshot(
id: json['id']?.toString() ?? '',
gameId: json['game_id']?.toString() ?? '',
screenshotPath: json['screenshot_path'] ?? '',
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'game_id': gameId,
'screenshot_path': screenshotPath,
'is_deleted': isDeleted ? 1 : 0,
'created_at': createdAt.toUtc().toIso8601String(),
};
}
File? get screenshotFile {
if (screenshotPath.isEmpty) return null;
return File(screenshotPath);
}
}
/// 书籍摘抄模型
class BookExcerpt {
final String id;
final String bookId;
final String chapter; // 章节
final String content; // 摘抄内容
final String comment; // 摘抄的评论/感悟
final bool isDeleted;
final DateTime createdAt;
final DateTime updatedAt;
BookExcerpt({
required this.id,
required this.bookId,
this.chapter = '',
required this.content,
this.comment = '',
this.isDeleted = false,
required this.createdAt,
required this.updatedAt,
});
factory BookExcerpt.fromJson(Map<String, dynamic> json) {
return BookExcerpt(
id: json['id']?.toString() ?? '',
bookId: json['book_id']?.toString() ?? '',
chapter: json['chapter'] ?? '',
content: json['content'] ?? '',
comment: json['comment'] ?? '',
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
updatedAt: _safeParseDate(json['updated_at'], fallback: DateTime.now())!,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'book_id': bookId,
'chapter': chapter,
'content': content,
'comment': comment,
'is_deleted': isDeleted ? 1 : 0,
'created_at': createdAt.toUtc().toIso8601String(),
'updated_at': updatedAt.toUtc().toIso8601String(),
};
}
/// 复制并修改
BookExcerpt copyWith({
String? id,
String? bookId,
String? chapter,
String? content,
String? comment,
bool? isDeleted,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return BookExcerpt(
id: id ?? this.id,
bookId: bookId ?? this.bookId,
chapter: chapter ?? this.chapter,
content: content ?? this.content,
comment: comment ?? this.comment,
isDeleted: isDeleted ?? this.isDeleted,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
/// 获取摘抄摘要
String get summary {
if (content.length <= 50) return content;
return '${content.substring(0, 50)}...';
}
}
/// 片单模型
class Playlist {
final String id;
final String name;
final String description;
final String type; // 'movie' / 'book' / 'game'
final String? coverPath;
final int itemCount;
final int sortOrder;
final DateTime createdAt;
final DateTime updatedAt;
final bool isDeleted;
Playlist({
required this.id,
required this.name,
this.description = '',
required this.type,
this.coverPath,
this.itemCount = 0,
this.sortOrder = 0,
required this.createdAt,
required this.updatedAt,
this.isDeleted = false,
});
factory Playlist.fromJson(Map<String, dynamic> json) {
return Playlist(
id: json['id']?.toString() ?? '',
name: json['name']?.toString() ?? '',
description: json['description']?.toString() ?? '',
type: json['type']?.toString() ?? 'movie',
coverPath: json['cover_path'],
itemCount: json['item_count'] ?? 0,
sortOrder: json['sort_order'] ?? 0,
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
updatedAt: _safeParseDate(json['updated_at'], fallback: DateTime.now())!,
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'description': description,
'type': type,
'cover_path': coverPath,
'item_count': itemCount,
'sort_order': sortOrder,
'created_at': createdAt.toUtc().toIso8601String(),
'updated_at': updatedAt.toUtc().toIso8601String(),
'is_deleted': isDeleted ? 1 : 0,
};
}
Playlist copyWith({
String? id,
String? name,
String? description,
String? type,
String? coverPath,
int? itemCount,
int? sortOrder,
DateTime? createdAt,
DateTime? updatedAt,
bool? isDeleted,
}) {
return Playlist(
id: id ?? this.id,
name: name ?? this.name,
description: description ?? this.description,
type: type ?? this.type,
coverPath: coverPath ?? this.coverPath,
itemCount: itemCount ?? this.itemCount,
sortOrder: sortOrder ?? this.sortOrder,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
isDeleted: isDeleted ?? this.isDeleted,
);
}
String get typeLabel => switch (type) {
'movie' => '影视',
'book' => '书籍',
'game' => '游戏',
_ => type,
};
}
/// 片单条目模型
class PlaylistItem {
final String id;
final String playlistId;
final String itemId;
final int sortOrder;
final DateTime addedAt;
PlaylistItem({
required this.id,
required this.playlistId,
required this.itemId,
this.sortOrder = 0,
required this.addedAt,
});
factory PlaylistItem.fromJson(Map<String, dynamic> json) {
return PlaylistItem(
id: json['id']?.toString() ?? '',
playlistId: json['playlist_id']?.toString() ?? '',
itemId: json['item_id']?.toString() ?? '',
sortOrder: json['sort_order'] ?? 0,
addedAt: _safeParseDate(json['added_at'], fallback: DateTime.now())!,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'playlist_id': playlistId,
'item_id': itemId,
'sort_order': sortOrder,
'added_at': addedAt.toUtc().toIso8601String(),
};
}
}
/// 人物档案模型
class Person {
final String id;
final String name;
final String? gender; // male/female/other
final DateTime? birthDate;
final String? birthPlace;
final List<String> alternateNames; // 其他名称
final List<String> occupation; // 职业: ["导演","演员","编剧"]
final String? summary;
final String? photoPath;
final double coverOffset;
final bool isDeleted;
final DateTime createdAt;
final DateTime updatedAt;
Person({
required this.id,
required this.name,
this.gender,
this.birthDate,
this.birthPlace,
this.alternateNames = const [],
this.occupation = const [],
this.summary,
this.photoPath,
this.coverOffset = 0.0,
this.isDeleted = false,
required this.createdAt,
required this.updatedAt,
});
factory Person.fromJson(Map<String, dynamic> json) {
return Person(
id: json['id'] ?? '',
name: json['name'] ?? '',
gender: json['gender'],
birthDate: _safeParseDate(json['birth_date']),
birthPlace: json['birth_place'],
alternateNames: parseStringListGeneric(json['alternate_names']),
occupation: parseStringListGeneric(json['occupation']),
summary: json['summary'],
photoPath: json['photo_path'],
coverOffset: _safeParseDouble(json['cover_offset'], fallback: 0.0)!,
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
updatedAt: _safeParseDate(json['updated_at'], fallback: DateTime.now())!,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'gender': gender,
'birth_date': birthDate?.toUtc().toIso8601String(),
'birth_place': birthPlace,
'alternate_names': jsonEncode(alternateNames),
'occupation': jsonEncode(occupation),
'summary': summary,
'photo_path': photoPath,
'cover_offset': coverOffset,
'is_deleted': isDeleted ? 1 : 0,
'created_at': createdAt.toUtc().toIso8601String(),
'updated_at': updatedAt.toUtc().toIso8601String(),
};
}
Person copyWith({
String? id,
String? name,
Object? gender = _copyWithNull,
DateTime? birthDate,
Object? birthPlace = _copyWithNull,
List<String>? alternateNames,
List<String>? occupation,
Object? summary = _copyWithNull,
Object? photoPath = _copyWithNull,
double? coverOffset,
bool? isDeleted,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return Person(
id: id ?? this.id,
name: name ?? this.name,
gender: gender is _CopyWithNullSentinel ? this.gender : (gender as String?),
birthDate: birthDate ?? this.birthDate,
birthPlace: birthPlace is _CopyWithNullSentinel ? this.birthPlace : (birthPlace as String?),
alternateNames: alternateNames ?? this.alternateNames,
occupation: occupation ?? this.occupation,
summary: summary is _CopyWithNullSentinel ? this.summary : (summary as String?),
photoPath: photoPath is _CopyWithNullSentinel ? this.photoPath : (photoPath as String?),
coverOffset: coverOffset ?? this.coverOffset,
isDeleted: isDeleted ?? this.isDeleted,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
/// 获取封面文件
File? get photoFile {
if (photoPath == null || photoPath!.isEmpty) return null;
return File(photoPath!);
}
}
/// 影视↔人物关联模型
class MoviePerson {
final String id;
final String movieId;
final String personId;
final String roleType; // director/writer/actor
final String? characterName; // 饰演角色仅actor
final int sortOrder;
MoviePerson({
required this.id,
required this.movieId,
required this.personId,
required this.roleType,
this.characterName,
this.sortOrder = 0,
});
factory MoviePerson.fromJson(Map<String, dynamic> json) {
return MoviePerson(
id: json['id'] ?? '',
movieId: json['movie_id'] ?? '',
personId: json['person_id'] ?? '',
roleType: json['role_type'] ?? 'actor',
characterName: json['character_name'],
sortOrder: json['sort_order'] ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'movie_id': movieId,
'person_id': personId,
'role_type': roleType,
'character_name': characterName,
'sort_order': sortOrder,
};
}
}
/// 书籍↔人物关联模型
class BookPerson {
final String id;
final String bookId;
final String personId;
final String roleType; // author/translator
final int sortOrder;
BookPerson({
required this.id,
required this.bookId,
required this.personId,
required this.roleType,
this.sortOrder = 0,
});
factory BookPerson.fromJson(Map<String, dynamic> json) {
return BookPerson(
id: json['id'] ?? '',
bookId: json['book_id'] ?? '',
personId: json['person_id'] ?? '',
roleType: json['role_type'] ?? 'author',
sortOrder: json['sort_order'] ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'book_id': bookId,
'person_id': personId,
'role_type': roleType,
'sort_order': sortOrder,
};
}
}
/// 游戏↔人物关联模型
class GamePerson {
final String id;
final String gameId;
final String personId;
final String roleType; // developer
final int sortOrder;
GamePerson({
required this.id,
required this.gameId,
required this.personId,
required this.roleType,
this.sortOrder = 0,
});
factory GamePerson.fromJson(Map<String, dynamic> json) {
return GamePerson(
id: json['id'] ?? '',
gameId: json['game_id'] ?? '',
personId: json['person_id'] ?? '',
roleType: json['role_type'] ?? 'developer',
sortOrder: json['sort_order'] ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'game_id': gameId,
'person_id': personId,
'role_type': roleType,
'sort_order': sortOrder,
};
}
}
/// 影视角色模型
class MovieCharacter {
final String id;
final String movieId;
final String name;
final String? role;
final List<String> aliases;
final List<String> tags;
final String? description;
final String? imagePath;
final int sortOrder;
final bool isDeleted;
final DateTime createdAt;
final DateTime updatedAt;
MovieCharacter({
required this.id,
required this.movieId,
required this.name,
this.role,
this.aliases = const [],
this.tags = const [],
this.description,
this.imagePath,
this.sortOrder = 0,
this.isDeleted = false,
required this.createdAt,
required this.updatedAt,
});
factory MovieCharacter.fromJson(Map<String, dynamic> json) {
return MovieCharacter(
id: json['id']?.toString() ?? '',
movieId: json['movie_id']?.toString() ?? '',
name: json['name'] ?? '',
role: json['role'],
aliases: parseStringListGeneric(json['aliases']),
tags: parseStringListGeneric(json['tags']),
description: json['description'],
imagePath: json['image_path'],
sortOrder: json['sort_order'] ?? 0,
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
updatedAt: _safeParseDate(json['updated_at'], fallback: DateTime.now())!,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'movie_id': movieId,
'name': name,
'role': role,
'aliases': jsonEncode(aliases),
'tags': jsonEncode(tags),
'description': description,
'image_path': imagePath,
'sort_order': sortOrder,
'is_deleted': isDeleted ? 1 : 0,
'created_at': createdAt.toUtc().toIso8601String(),
'updated_at': updatedAt.toUtc().toIso8601String(),
};
}
MovieCharacter copyWith({
String? id,
String? movieId,
String? name,
Object? role = _copyWithNull,
List<String>? aliases,
List<String>? tags,
Object? description = _copyWithNull,
Object? imagePath = _copyWithNull,
int? sortOrder,
bool? isDeleted,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return MovieCharacter(
id: id ?? this.id,
movieId: movieId ?? this.movieId,
name: name ?? this.name,
role: role is _CopyWithNullSentinel ? this.role : (role as String?),
aliases: aliases ?? this.aliases,
tags: tags ?? this.tags,
description: description is _CopyWithNullSentinel ? this.description : (description as String?),
imagePath: imagePath is _CopyWithNullSentinel ? this.imagePath : (imagePath as String?),
sortOrder: sortOrder ?? this.sortOrder,
isDeleted: isDeleted ?? this.isDeleted,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
File? get imageFile {
if (imagePath == null || imagePath!.isEmpty) return null;
return File(imagePath!);
}
String get summary {
if (description == null || description!.isEmpty) return '';
if (description!.length <= 50) return description!;
return '${description!.substring(0, 50)}...';
}
}
/// 书籍角色模型
class BookCharacter {
final String id;
final String bookId;
final String name;
final String? role;
final List<String> aliases;
final List<String> tags;
final String? description;
final String? imagePath;
final int sortOrder;
final bool isDeleted;
final DateTime createdAt;
final DateTime updatedAt;
BookCharacter({
required this.id,
required this.bookId,
required this.name,
this.role,
this.aliases = const [],
this.tags = const [],
this.description,
this.imagePath,
this.sortOrder = 0,
this.isDeleted = false,
required this.createdAt,
required this.updatedAt,
});
factory BookCharacter.fromJson(Map<String, dynamic> json) {
return BookCharacter(
id: json['id']?.toString() ?? '',
bookId: json['book_id']?.toString() ?? '',
name: json['name'] ?? '',
role: json['role'],
aliases: parseStringListGeneric(json['aliases']),
tags: parseStringListGeneric(json['tags']),
description: json['description'],
imagePath: json['image_path'],
sortOrder: json['sort_order'] ?? 0,
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
updatedAt: _safeParseDate(json['updated_at'], fallback: DateTime.now())!,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'book_id': bookId,
'name': name,
'role': role,
'aliases': jsonEncode(aliases),
'tags': jsonEncode(tags),
'description': description,
'image_path': imagePath,
'sort_order': sortOrder,
'is_deleted': isDeleted ? 1 : 0,
'created_at': createdAt.toUtc().toIso8601String(),
'updated_at': updatedAt.toUtc().toIso8601String(),
};
}
BookCharacter copyWith({
String? id,
String? bookId,
String? name,
Object? role = _copyWithNull,
List<String>? aliases,
List<String>? tags,
Object? description = _copyWithNull,
Object? imagePath = _copyWithNull,
int? sortOrder,
bool? isDeleted,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return BookCharacter(
id: id ?? this.id,
bookId: bookId ?? this.bookId,
name: name ?? this.name,
role: role is _CopyWithNullSentinel ? this.role : (role as String?),
aliases: aliases ?? this.aliases,
tags: tags ?? this.tags,
description: description is _CopyWithNullSentinel ? this.description : (description as String?),
imagePath: imagePath is _CopyWithNullSentinel ? this.imagePath : (imagePath as String?),
sortOrder: sortOrder ?? this.sortOrder,
isDeleted: isDeleted ?? this.isDeleted,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
File? get imageFile {
if (imagePath == null || imagePath!.isEmpty) return null;
return File(imagePath!);
}
String get summary {
if (description == null || description!.isEmpty) return '';
if (description!.length <= 50) return description!;
return '${description!.substring(0, 50)}...';
}
}
/// 游戏角色模型
class GameCharacter {
final String id;
final String gameId;
final String name;
final String? role;
final List<String> aliases;
final List<String> tags;
final String? description;
final String? imagePath;
final int sortOrder;
final bool isDeleted;
final DateTime createdAt;
final DateTime updatedAt;
GameCharacter({
required this.id,
required this.gameId,
required this.name,
this.role,
this.aliases = const [],
this.tags = const [],
this.description,
this.imagePath,
this.sortOrder = 0,
this.isDeleted = false,
required this.createdAt,
required this.updatedAt,
});
factory GameCharacter.fromJson(Map<String, dynamic> json) {
return GameCharacter(
id: json['id']?.toString() ?? '',
gameId: json['game_id']?.toString() ?? '',
name: json['name'] ?? '',
role: json['role'],
aliases: parseStringListGeneric(json['aliases']),
tags: parseStringListGeneric(json['tags']),
description: json['description'],
imagePath: json['image_path'],
sortOrder: json['sort_order'] ?? 0,
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
updatedAt: _safeParseDate(json['updated_at'], fallback: DateTime.now())!,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'game_id': gameId,
'name': name,
'role': role,
'aliases': jsonEncode(aliases),
'tags': jsonEncode(tags),
'description': description,
'image_path': imagePath,
'sort_order': sortOrder,
'is_deleted': isDeleted ? 1 : 0,
'created_at': createdAt.toUtc().toIso8601String(),
'updated_at': updatedAt.toUtc().toIso8601String(),
};
}
GameCharacter copyWith({
String? id,
String? gameId,
String? name,
Object? role = _copyWithNull,
List<String>? aliases,
List<String>? tags,
Object? description = _copyWithNull,
Object? imagePath = _copyWithNull,
int? sortOrder,
bool? isDeleted,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return GameCharacter(
id: id ?? this.id,
gameId: gameId ?? this.gameId,
name: name ?? this.name,
role: role is _CopyWithNullSentinel ? this.role : (role as String?),
aliases: aliases ?? this.aliases,
tags: tags ?? this.tags,
description: description is _CopyWithNullSentinel ? this.description : (description as String?),
imagePath: imagePath is _CopyWithNullSentinel ? this.imagePath : (imagePath as String?),
sortOrder: sortOrder ?? this.sortOrder,
isDeleted: isDeleted ?? this.isDeleted,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
File? get imageFile {
if (imagePath == null || imagePath!.isEmpty) return null;
return File(imagePath!);
}
String get summary {
if (description == null || description!.isEmpty) return '';
if (description!.length <= 50) return description!;
return '${description!.substring(0, 50)}...';
}
}