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,30 @@
// Author: fruit.com.cn
import '../models/content_category.dart';
import '../models/home_feed.dart';
import '../remote/category_api.dart';
import 'catalog_repository.dart';
/// 发现页分类数据来源:分类清单与发现 feed。
abstract interface class CategoryRepository {
/// 拉取服务端分类。
Future<List<ContentCategory>> fetchCategories();
/// 拉取发现页分区;分类内容只从这里读取。
Future<HomeFeed> fetchDiscoverFeed();
}
/// HTTP 实现。发现 feed 复用既有目录仓库,不新增接口。
class HttpCategoryRepository implements CategoryRepository {
const HttpCategoryRepository(this._categoryApi, this._catalogRepository);
final CategoryApi _categoryApi;
final CatalogRepository _catalogRepository;
@override
Future<List<ContentCategory>> fetchCategories() => _categoryApi.list();
@override
Future<HomeFeed> fetchDiscoverFeed() =>
_catalogRepository.fetchDiscoverFeed();
}
@@ -0,0 +1,82 @@
// Author: fruit.com.cn
import '../models/download_license.dart';
import '../models/download_policy.dart';
import '../remote/download_api.dart';
/// 下载授权数据来源,页面只依赖该抽象。
abstract interface class DownloadRepository {
Future<DownloadPolicy> fetchPolicy();
Future<List<DownloadLicense>> list();
Future<DownloadLicense> grant(String episodeId, {String? quality});
Future<void> updateSettings({bool? wifiOnly, String? maxQuality});
Future<DownloadLicense> refreshUrl(String episodeId);
Future<DownloadLicense> reportProgress(
String episodeId, {
String? deviceId,
String? status,
int? progressPercent,
int? downloadedBytes,
int? totalBytes,
String? errorMsg,
});
Future<void> remove(String episodeId);
Future<int> clear();
}
/// HTTP 下载授权仓库。
class HttpDownloadRepository implements DownloadRepository {
const HttpDownloadRepository(this._api);
final DownloadApi _api;
@override
Future<DownloadPolicy> fetchPolicy() => _api.policy();
@override
Future<List<DownloadLicense>> list() => _api.list();
@override
Future<DownloadLicense> grant(String episodeId, {String? quality}) =>
_api.grant(episodeId, quality: quality);
@override
Future<void> updateSettings({bool? wifiOnly, String? maxQuality}) =>
_api.updateSettings(wifiOnly: wifiOnly, maxQuality: maxQuality);
@override
Future<DownloadLicense> refreshUrl(String episodeId) =>
_api.refreshUrl(episodeId);
@override
Future<DownloadLicense> reportProgress(
String episodeId, {
String? deviceId,
String? status,
int? progressPercent,
int? downloadedBytes,
int? totalBytes,
String? errorMsg,
}) => _api.reportProgress(
episodeId,
deviceId: deviceId,
status: status,
progressPercent: progressPercent,
downloadedBytes: downloadedBytes,
totalBytes: totalBytes,
errorMsg: errorMsg,
);
@override
Future<void> remove(String episodeId) => _api.remove(episodeId);
@override
Future<int> clear() => _api.clear();
}
@@ -47,7 +47,9 @@ class InMemoryFavoriteRepository implements FavoriteRepository {
final List<Drama> _cards;
bool failToggle = false;
bool failList = false;
bool failRemove = false;
int toggleCalls = 0;
int listCalls = 0;
@override
Future<bool> toggle(String dramaId) async {
@@ -59,10 +61,14 @@ class InMemoryFavoriteRepository implements FavoriteRepository {
@override
Future<List<Drama>> list() async {
listCalls++;
if (failList) throw StateError('favorite unavailable');
return _cards.where((card) => _saved.contains(card.id)).toList();
}
@override
Future<void> remove(String dramaId) async => _saved.remove(dramaId);
Future<void> remove(String dramaId) async {
if (failRemove) throw StateError('favorite unavailable');
_saved.remove(dramaId);
}
}
@@ -25,6 +25,9 @@ abstract interface class WatchHistoryRepository {
/// 清理某部剧的记录(K2 的记录页会用,这里先备好)。
Future<void> remove(String dramaId);
/// 清空当前账号的全部观看记录。
Future<void> clear();
}
class HttpWatchHistoryRepository implements WatchHistoryRepository {
@@ -70,12 +73,20 @@ class HttpWatchHistoryRepository implements WatchHistoryRepository {
if (!_signedIn) return;
await _api.removeProgress(dramaId);
}
@override
Future<void> clear() async {
if (!_signedIn) return;
await _api.clearProgress();
}
}
/// 内存实现:单测与 widget 测试注入用(不依赖网络与偏好存储)。
class InMemoryWatchHistoryRepository implements WatchHistoryRepository {
InMemoryWatchHistoryRepository({List<WatchProgress>? seed})
: _records = {for (final item in seed ?? const <WatchProgress>[]) item.dramaId: item};
: _records = {
for (final item in seed ?? const <WatchProgress>[]) item.dramaId: item,
};
final Map<String, WatchProgress> _records;
int failReads = 0;
@@ -122,6 +133,9 @@ class InMemoryWatchHistoryRepository implements WatchHistoryRepository {
@override
Future<void> remove(String dramaId) async => _records.remove(dramaId);
@override
Future<void> clear() async => _records.clear();
}
/// 续看点辅助:把服务端记录换算成"第几集 + 从哪一秒开始"。