Skip to content

市场 UI

市场 UI 在 lib/features/market/ 下,三个页面 + 一个共享组件。全部用 Material Design 3 Expressive + Claymorphism 设计系统(复用 app.dart 里的 ClayContainer / IconBadge / tokens)。

  • market_page.dart — 市场首页(3 Tab + 卡片网格)
  • plugin_detail_page.dart — 插件详情页(Friend / Application)
  • pack_detail_page.dart — 整合包详情页
  • preview_image.dart — 共享预览图组件

ConsumerStatefulWidget + SingleTickerProviderStateMixin + TabController(length: 3)。

  • AppBar:标题“插件市场” + refresh 按钮(叠 Badge 红点,当有可用更新时)
  • TabBar:三个 Tab — 伙伴 / 应用 / 整合包
  • TabBarView:三个 _MarketTab

每个 Tab 是一个 ConsumerWidget:

  • RefreshIndicator 包 ref.watch(provider).when(loading/error/data)
  • loading:CircularProgressIndicator
  • error:cloud_off 图标 + “加载失败” + 错误信息
  • data:GridView.builder(maxCrossAxisExtent: 500,自适应列数)

ConsumerWidget,每个插件一张卡片:

  • ClayContainer(kRadiusLg,shadowIntensity: 0.6,tap 进详情页)
  • 顶部:PreviewImage(Expanded 填充卡片剩余空间,BoxFit.cover)
  • 底部:名称 / 作者 / 描述(2 行)/ 评分 + 安装按钮

安装按钮三态:

状态 按钮
未安装 FilledButton("安装")
已安装 + 有更新 FilledButton.tonal("更新")
已安装 + 无更新 OutlinedButton("已安装")(禁用)
安装中 CircularProgressIndicator(18×18)
final hasUpdates = ref.watch(marketUpdatesProvider).maybeWhen(
data: (updates) => updates.isNotEmpty,
orElse: () => false,
);
Badge(
isLabelVisible: hasUpdates,
backgroundColor: theme.colorScheme.error,
child: const Icon(Icons.refresh_rounded),
)

有可用更新时 refresh 图标右上角显示红色小点。

Future<void> _install(MarketPlugin plugin) async {
if (_installing.contains(plugin.id)) return;
final manager = ref.read(pluginManagerProvider).maybeWhen(data: (m) => m, orElse: () => null);
final isUpdate = manager?.pluginById(plugin.id) != null;
setState(() => _installing.add(plugin.id));
try {
final installer = await ref.read(pluginInstallerProvider.future);
final result = await installer.install(plugin);
if (result.success) {
SnackBar(isUpdate ? '更新成功' : '安装成功');
ref.invalidate(pluginManagerProvider);
ref.invalidate(marketUpdatesProvider);
} else {
SnackBar('安装失败: ${result.error}');
}
} finally {
setState(() => _installing.remove(plugin.id));
}
}
void _openDetail(MarketPlugin plugin) {
Navigator.pushNamed(
context,
plugin.type == PluginType.pack ? '/market/pack' : '/market/plugin',
arguments: plugin,
);
}

ConsumerStatefulWidget。路由 /market/plugin,参数是 MarketPlugin。

final args = ModalRoute.of(context)?.settings.arguments;
if (args is! MarketPlugin) {
return Scaffold(body: Center(child: Text('参数错误')));
}

SingleChildScrollView 纵向滚动:

  1. 预览图:ClayContainer(kRadiusXl) + AspectRatio(1) + PreviewImage
  2. 名称:headlineSmall w700
  3. 作者 + 版本 chip:bodyMedium + Chip("v1.0.0")
  4. 描述:bodyLarge height 1.6
  5. 平台 chips:Wrap of Chip,当前平台用 primaryContainer 高亮
  6. 统计行:三个 _StatTile(文件大小 / 下载量 / 评分)
  7. 源码链接:SelectableText 显示 GitHub raw URL(无 url_launcher,用户自己复制)
  8. 安装按钮:全宽 FilledButton.icon
状态 按钮
未安装 FilledButton.icon(Icons.download, "安装")
已安装 + 有更新 FilledButton.tonalIcon(Icons.upgrade, "更新")
已安装 + 无更新 OutlinedButton.icon(Icons.check, "已安装")(禁用)
安装中 FilledButton.icon(Icons.downloading, "安装中…")(禁用)+ LinearProgressIndicator(value: _progress)

_progress 来自 PluginInstaller.install(onProgress) 的回调——流式下载时按字节进度更新。

PackDetailPage 复用 PluginDetailBody(isPack: true),加一条说明:

整合包包含一组 Friend / Application 插件,点击安装将一并装入。

不展示子插件列表(子插件信息在 zip 内,不在 meta.json 里)。安装走同一个 PluginInstaller.install,内部调 installPack 展开子插件。

ConsumerStatefulWidget,共享组件。参数 MarketDir dir + String id。

PreviewImage(dir: plugin.dir, id: plugin.id)
  • initState 里异步调 repository.fetchPreview(dir, id) 拿缓存 File
  • 拿到后 Image.file(file, fit: BoxFit.cover, gaplessPlayback: true)
  • 加载中:CircularProgressIndicator(背景 surfaceContainerHighest(0.3))
  • 失败:IconBadge(Icons.image_outlined) 占位

跨卡片和详情页复用,避免重复加载。

在 lib/app.dart 的 MoodPetApp.routes 里:

routes: {
'/plugins': (context) => const PluginManagementPage(),
'/settings': (context) => const SettingsPage(),
'/market': (context) => const MarketPage(),
'/market/plugin': (context) => const PluginDetailPage(),
'/market/pack': (context) => const PackDetailPage(),
},

/market 从插件管理页的 AppBar store 图标 push 进来。