feat(flutter): 接入分类记录收藏与下载授权
This commit is contained in:
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? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user