feat(flutter): 接入分类记录收藏与下载授权

This commit is contained in:
Zhou Weidong committed 2026-10-17 14:49:16 +08:00
1 parent 00280ca0da
commit cc4d7c5a9d
52 files changed
+4440 -171

No files matched your search

@@ -0,0 +1,17 @@
// Author: fruit.com.cn
/// 服务端内容分类。id 按契约保持字符串,避免雪花 ID 精度丢失。
class ContentCategory {
const ContentCategory({required this.id, required this.name});
final String id;
final String name;
/// 解析 `GET /app/categories` 的单项。
factory ContentCategory.fromJson(Map<String, dynamic> json) {
return ContentCategory(
id: (json['id'] ?? '').toString(),
name: json['name'] as String? ?? '',
);
}
}
@@ -0,0 +1,67 @@
// Author: fruit.com.cn
import 'json_numbers.dart';
/// 下载授权与任务进度合并视图,对应 `GET /app/downloads` 的列表项。
class DownloadLicense {
const DownloadLicense({
required this.licenseId,
required this.dramaId,
required this.dramaTitle,
required this.episodeId,
required this.episodeNo,
required this.deviceId,
required this.quality,
required this.playUrl,
required this.licenseKey,
required this.status,
required this.grantedAt,
required this.expireAt,
required this.taskStatus,
required this.progressPercent,
required this.downloadedBytes,
required this.totalBytes,
required this.errorMsg,
});
final String licenseId;
final String dramaId;
final String dramaTitle;
final String episodeId;
final int episodeNo;
final String deviceId;
final String quality;
final String playUrl;
final String licenseKey;
final String status;
final DateTime? grantedAt;
final DateTime? expireAt;
final String taskStatus;
final int progressPercent;
final int downloadedBytes;
final int totalBytes;
final String errorMsg;
/// 解析授权响应;所有 ID 与 Long 字段兼容字符串下发。
factory DownloadLicense.fromJson(Map<String, dynamic> json) {
return DownloadLicense(
licenseId: (json['licenseId'] ?? '').toString(),
dramaId: (json['dramaId'] ?? '').toString(),
dramaTitle: json['dramaTitle'] as String? ?? '',
episodeId: (json['episodeId'] ?? '').toString(),
episodeNo: jsonToInt(json['episodeNo']),
deviceId: json['deviceId'] as String? ?? '',
quality: json['quality'] as String? ?? '',
playUrl: json['playUrl'] as String? ?? '',
licenseKey: json['licenseKey'] as String? ?? '',
status: json['status'] as String? ?? '',
grantedAt: DateTime.tryParse(json['grantedAt'] as String? ?? ''),
expireAt: DateTime.tryParse(json['expireAt'] as String? ?? ''),
taskStatus: json['taskStatus'] as String? ?? '',
progressPercent: jsonToInt(json['progressPercent']),
downloadedBytes: jsonToInt(json['downloadedBytes']),
totalBytes: jsonToInt(json['totalBytes']),
errorMsg: json['errorMsg'] as String? ?? '',
);
}
}
+54
View File
@@ -0,0 +1,54 @@
// Author: fruit.com.cn
import 'json_numbers.dart';
/// 下载策略与当前设备配额,对应 `GET /app/downloads/policy`。
class DownloadPolicy {
const DownloadPolicy({
required this.maxPerDevice,
required this.licenseDays,
required this.wifiOnly,
required this.maxQuality,
required this.maxStorageBytes,
required this.used,
required this.remaining,
required this.expirePolicyText,
});
final int maxPerDevice;
final int licenseDays;
final bool wifiOnly;
final String maxQuality;
final int maxStorageBytes;
final int used;
final int remaining;
final String expirePolicyText;
/// 设置更新期间用于本地乐观展示。
DownloadPolicy copyWith({bool? wifiOnly, String? maxQuality}) {
return DownloadPolicy(
maxPerDevice: maxPerDevice,
licenseDays: licenseDays,
wifiOnly: wifiOnly ?? this.wifiOnly,
maxQuality: maxQuality ?? this.maxQuality,
maxStorageBytes: maxStorageBytes,
used: used,
remaining: remaining,
expirePolicyText: expirePolicyText,
);
}
/// 解析策略响应;Long 字段兼容字符串。
factory DownloadPolicy.fromJson(Map<String, dynamic> json) {
return DownloadPolicy(
maxPerDevice: jsonToInt(json['maxPerDevice']),
licenseDays: jsonToInt(json['licenseDays']),
wifiOnly: json['wifiOnly'] == true,
maxQuality: json['maxQuality'] as String? ?? '',
maxStorageBytes: jsonToInt(json['maxStorageBytes']),
used: jsonToInt(json['used']),
remaining: jsonToInt(json['remaining']),
expirePolicyText: json['expirePolicyText'] as String? ?? '',
);
}
}
+4
View File
@@ -14,6 +14,7 @@ class WatchProgress {
required this.duration,
this.finished = false,
this.playCount = 0,
this.updatedAt,
});
final String dramaId;
@@ -22,6 +23,7 @@ class WatchProgress {
final Duration duration;
final bool finished;
final int playCount;
final DateTime? updatedAt;
bool get hasPosition => position > Duration.zero;
@@ -39,6 +41,7 @@ class WatchProgress {
duration: Duration(milliseconds: jsonToInt(json['durationMs'])),
finished: json['finished'] == true,
playCount: jsonToInt(json['playCount']),
updatedAt: DateTime.tryParse(json['updatedAt'] as String? ?? ''),
);
}
@@ -49,6 +52,7 @@ class WatchProgress {
'durationMs': duration.inMilliseconds,
'finished': finished,
'playCount': playCount,
if (updatedAt != null) 'updatedAt': updatedAt!.toIso8601String(),
};
}
+44
View File
@@ -0,0 +1,44 @@
// Author: fruit.com.cn
import 'drama.dart';
import 'watch_progress.dart';
/// 观看记录页的一条数据:进度记录与按 dramaId 补取的详情卡片。
class WatchRecord {
const WatchRecord({
required this.progress,
this.drama,
this.detailLoading = true,
this.detailFailed = false,
});
final WatchProgress progress;
final Drama? drama;
final bool detailLoading;
final bool detailFailed;
String get dramaId => progress.dramaId;
/// 详情中按 episodeId 定位集号;缺失时返回 0。
int get episodeNumber {
final episodes = drama?.episodes ?? const <Episode>[];
for (var index = 0; index < episodes.length; index++) {
if (episodes[index].id == progress.episodeId) return index + 1;
}
return 0;
}
/// 生成卡片加载中、成功或失败后的新状态。
WatchRecord copyWith({
Drama? drama,
bool? detailLoading,
bool? detailFailed,
}) {
return WatchRecord(
progress: progress,
drama: drama ?? this.drama,
detailLoading: detailLoading ?? this.detailLoading,
detailFailed: detailFailed ?? this.detailFailed,
);
}
}