Skip to content

MarketRepository

MarketRepository 是市场数据层核心——负责和 GitHub 说话,从 Contents API 拉目录列表,从 raw URL 拉元数据/包/预览图。所有网络调用走 package:http,失败抛 MarketException。缓存委托给 MarketCache。

文件:lib/core/market/market_repository.dart

class MarketPlugin {
final String id;
final PluginType type;
final String name;
final String description;
final String version;
final String author;
final List<PlatformId> platforms;
final String? icon;
final String? fileSize;
final int downloads;
final double rating;
final MarketDir dir;
}

.meta.json 解析后的类型化模型。字段和 manifest 公共字段对齐,加展示专用字段。

factory MarketPlugin.fromJson(Map<String, Object?> json, {required MarketDir dir})

严格解析——缺 id/type/name/description/version/author 任一抛 FormatException。platforms 必须是数组,每个值必须是合法平台 id(否则 PlatformId.fromString 抛 ArgumentError)。

final plugin = MarketPlugin.fromJson(json, dir: MarketDir.friend);
class MarketRepository {
final MarketConfig config;
final MarketCache cache;
MarketRepository(this.config, this.cache, {http.Client? client});
void dispose();
}

构造时可选注入 http.Client(测试用)。不注入则自建一个,dispose() 时关闭。

Future<List<MarketPlugin>> listDirectory(MarketDir dir)

列出某个目录的所有插件(含完整元数据)。流程:

  1. 查目录列表缓存(5 分钟 TTL)
  2. 未命中 → Contents API 拉文件列表 → 提取 .meta.json 文件名 → 得到 id 列表 → 缓存
  3. 对每个 id 调 fetchMeta(1 小时 TTL 缓存)
  4. 返回 List<MarketPlugin>

某个插件的 meta 拉失败会跳过(不中断整个列表)。

final repo = MarketRepository(config, cache);
final friends = await repo.listDirectory(MarketDir.friend);
for (final p in friends) {
print('${p.name} v${p.version} by ${p.author}');
}
Future<List<MarketPlugin>> refreshDirectory(MarketDir dir)

强制刷新:先 invalidateDirectory,再 listDirectory。下拉刷新用。

final friends = await repo.refreshDirectory(MarketDir.friend);
Future<MarketPlugin> fetchMeta(MarketDir dir, String id)

拉单个插件元数据。先查缓存(1 小时 TTL),未命中走 raw URL。失败抛 MarketException。

final plugin = await repo.fetchMeta(MarketDir.friend, 'moodpet.friend.default_smiley');
print(plugin.description);
Future<File> downloadPackage(
MarketDir dir,
String id, {
void Function(double progress)? onProgress,
})

下载插件包到磁盘缓存(永久),返回 File。流式下载——用 http.Client.send 拿 StreamedResponse,按 chunk 累积,据 Content-Length 调 onProgress(0.0–1.0)。

final file = await repo.downloadPackage(
MarketDir.friend,
'moodpet.friend.default_smiley',
onProgress: (p) => print('下载进度: ${(p * 100).toInt()}%'),
);
print('已下载到: ${file.path}');

如果已缓存(之前下过),直接返回 File,不触发网络和 onProgress。

Future<File> fetchPreview(MarketDir dir, String id)

拉预览图到磁盘缓存(7 天 TTL),返回 File。不走流式(图片小),用一次性 http.get。

final file = await repo.fetchPreview(MarketDir.friend, 'moodpet.friend.default_smiley');
// UI: Image.file(file, fit: BoxFit.cover)

已缓存且 < 7 天直接返回。

  • API 调用(Contents API)用 config.apiHeaders(含 Authorization)
  • Raw 下载用 config.rawHeaders(无 Authorization)
  • 非 200 状态码抛 MarketException('HTTP $code: ...', code)
  • JSON 解析失败抛 MarketException('response is not valid JSON', code)
  • 超时:API 调用 20 秒,包下载 60 秒,图下载 60 秒
final config = MarketConfig.defaultConfig;
final cache = MarketCache(cacheDir);
final repo = MarketRepository(config, cache);
try {
// 列出所有 Friend 插件
final friends = await repo.listDirectory(MarketDir.friend);
for (final plugin in friends) {
print('${plugin.name} - ${plugin.description}');
// 拉预览图
final image = await repo.fetchPreview(plugin.dir, plugin.id);
// 下载包
final package = await repo.downloadPackage(
plugin.dir,
plugin.id,
onProgress: (p) => updateProgressBar(p),
);
}
} on MarketException catch (e) {
print('市场请求失败: ${e.message}');
}
repo.dispose();