generated from dellevin/template
1727 lines
51 KiB
Dart
1727 lines
51 KiB
Dart
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 int duration; // 影视总时长(分钟)
|
||
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,
|
||
this.duration = 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,
|
||
duration: json['duration'] ?? 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,
|
||
'duration': duration,
|
||
'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,
|
||
int? duration,
|
||
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,
|
||
duration: duration ?? this.duration,
|
||
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)}...';
|
||
}
|
||
}
|
||
|
||
/// 图库图片项
|
||
class GalleryItem {
|
||
final String path;
|
||
final String category;
|
||
final String entityType;
|
||
final String entityId;
|
||
final String entityTitle;
|
||
final String? parentTitle;
|
||
final DateTime createdAt;
|
||
|
||
// category 取值:
|
||
// 'movie_poster' | 'movie_posters' | 'book_cover' | 'note_image'
|
||
// | 'game_cover' | 'game_screenshot' | 'person_photo'
|
||
// | 'movie_character' | 'book_character' | 'game_character'
|
||
//
|
||
// entityType 与 category 的映射:
|
||
// movie_poster / movie_posters / movie_character → 'movie'
|
||
// book_cover / book_character → 'book'
|
||
// note_image → 'note'
|
||
// game_cover / game_screenshot / game_character → 'game'
|
||
// person_photo → 'person'
|
||
//
|
||
// 角色图片:entityId 存父作品 ID,entityTitle 存角色名,parentTitle 存父作品标题
|
||
|
||
const GalleryItem({
|
||
required this.path,
|
||
required this.category,
|
||
required this.entityType,
|
||
required this.entityId,
|
||
required this.entityTitle,
|
||
this.parentTitle,
|
||
required this.createdAt,
|
||
});
|
||
}
|
||
|