import 'package:flutter/material.dart'; import '../models/model_manager.dart'; /// 设置弹层「模型清单」区块:顶部「识别模式」分段控件(s 高识别 / n 高性能, /// 默认 s,持久化本地;切换即热加载该档位已激活模型),下方 2 列封面缩略图网格。 /// 双档位:每数据集至多两张卡片(s/n 各一),卡片带档位角标; /// 卡片状态(下载进度/激活)按 (数据集, 档位) 独立记账。 /// /// 卡片状态机(档位 == 当前识别档位时与原单档行为一致): /// - 未下载 →「使用/下载」:下载完成自动激活(当前档立即使用,非当前档备好待切); /// - 已下载未激活 →「使用」直接激活;已激活且当前档 →「已使用」点击取消; /// - 已激活但属非当前档 →「已备好」(已下载就绪,点击切换到该档立即生效)。 class ModelCatalogSection extends StatelessWidget { final ModelManager manager; const ModelCatalogSection({super.key, required this.manager}); @override Widget build(BuildContext context) { return ListenableBuilder( listenable: manager, builder: (context, _) { // 目录行按 (数据集, 档位) 稳定排序:同数据集 s 前 n 后相邻展示 final items = [...manager.catalog]..sort((a, b) { if (a.datasetId != b.datasetId) { return a.datasetId.compareTo(b.datasetId); } return a.variant.compareTo(b.variant); }); return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( children: [ const Text('识别模式', style: TextStyle(color: Colors.white70, fontSize: 14)), const Spacer(), _ModeToggle(manager: manager), ], ), const SizedBox(height: 2), const Text('切换即加载该档位已激活模型(激活状态跨档保留)', style: TextStyle(color: Colors.white38, fontSize: 11)), const SizedBox(height: 12), Row( children: [ const Text('模型清单', style: TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold)), const Spacer(), TextButton.icon( onPressed: () => manager.refresh(), icon: const Icon(Icons.refresh, size: 16), label: const Text('刷新'), style: TextButton.styleFrom( foregroundColor: Colors.white70, visualDensity: VisualDensity.compact), ), ], ), const SizedBox(height: 4), if (manager.error != null) Padding( padding: const EdgeInsets.only(bottom: 8), child: Text(manager.error!, style: const TextStyle( color: Colors.orange, fontSize: 12)), ), if (items.isEmpty) const Text('暂无已发布模型', style: TextStyle(color: Colors.white54, fontSize: 13)) else GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 0.72, ), itemCount: items.length, itemBuilder: (context, i) => _ModelCard(item: items[i], manager: manager), ), ], ); }, ); } } /// 档位分段控件:s 高识别 / n 高性能 class _ModeToggle extends StatelessWidget { final ModelManager manager; const _ModeToggle({required this.manager}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( color: Colors.white12, borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ for (final v in const [kVariantS, kVariantN]) ...[ if (v != kVariantS) const SizedBox(width: 2), _seg(v), ], ], ), ); } Widget _seg(String v) { final selected = manager.mode == v; final label = v == kVariantS ? 's 高识别' : 'n 高性能'; return InkWell( borderRadius: BorderRadius.circular(6), onTap: () => manager.setMode(v), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: selected ? Colors.greenAccent : Colors.transparent, borderRadius: BorderRadius.circular(6), ), child: Text( label, style: TextStyle( color: selected ? Colors.black : Colors.white70, fontSize: 12, fontWeight: selected ? FontWeight.bold : FontWeight.normal, ), ), ), ); } } class _ModelCard extends StatelessWidget { final ModelCatalogItem item; final ModelManager manager; const _ModelCard({required this.item, required this.manager}); @override Widget build(BuildContext context) { final variant = item.variant; final sameMode = manager.mode == variant; final active = manager.isActive(item.datasetId, variant); final downloaded = manager.isDownloaded(item.datasetId, variant); final progress = manager.progressOf(item.datasetId, variant); final error = manager.errorOf(item.datasetId, variant); final accent = variant == kVariantS ? Colors.greenAccent : Colors.orangeAccent; final thumb = ClipRRect( borderRadius: BorderRadius.circular(8), child: AspectRatio( aspectRatio: 4 / 3, child: Stack( fit: StackFit.expand, children: [ Image.network( '${manager.baseUrl}${item.coverUrl}', fit: BoxFit.cover, loadingBuilder: (context, child, chunk) => chunk == null ? child : Container( color: Colors.white12, child: const Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2)))), errorBuilder: (context, error, stack) => Container( color: Colors.white12, child: const Icon(Icons.image_not_supported_outlined, color: Colors.white38), ), ), // 档位角标:s 高识别(绿)/ n 高性能(橙) Positioned( top: 4, left: 4, child: Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), decoration: BoxDecoration( color: accent, borderRadius: BorderRadius.circular(4), ), child: Text( variant == kVariantS ? 's' : 'n', style: const TextStyle( color: Colors.black87, fontSize: 10, fontWeight: FontWeight.bold), ), ), ), ], ), ), ); Widget action; if (progress != null) { action = Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ LinearProgressIndicator( value: progress, backgroundColor: Colors.white12, color: Colors.greenAccent), const SizedBox(height: 2), Row( children: [ Expanded( child: Text( '${(progress * 100).toStringAsFixed(0)}%', textAlign: TextAlign.center, style: const TextStyle(color: Colors.white70, fontSize: 11), ), ), TextButton( onPressed: () => manager.cancelDownload(item.datasetId, variant), style: TextButton.styleFrom( foregroundColor: Colors.white54, visualDensity: VisualDensity.compact, padding: const EdgeInsets.symmetric(horizontal: 8), minimumSize: const Size(0, 24), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: const Text('取消', style: TextStyle(fontSize: 11)), ), ], ), ], ); } else if (error != null) { action = Column( mainAxisSize: MainAxisSize.min, children: [ Text(error, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: Colors.redAccent, fontSize: 10)), TextButton( onPressed: () => manager.downloadModel(item), child: const Text('重试', style: TextStyle(fontSize: 12)), ), ], ); } else if (active && downloaded) { if (sameMode) { // 当前档已激活:点击取消 action = SizedBox( height: 30, child: OutlinedButton( onPressed: () => manager.setActive(item.datasetId, variant, false), style: OutlinedButton.styleFrom( foregroundColor: Colors.greenAccent, side: const BorderSide(color: Colors.greenAccent)), child: const Text('已使用', style: TextStyle(fontSize: 12)), ), ); } else { // 非当前档已备好(激活保留):点击切换到该档立即生效 action = SizedBox( height: 30, child: OutlinedButton( onPressed: () => manager.setMode(variant), style: OutlinedButton.styleFrom( foregroundColor: Colors.amberAccent, side: const BorderSide(color: Colors.amberAccent)), child: const Text('已备好', style: TextStyle(fontSize: 12)), ), ); } } else if (downloaded) { action = SizedBox( height: 30, child: FilledButton( onPressed: () => manager.setActive(item.datasetId, variant, true), style: FilledButton.styleFrom( backgroundColor: Colors.greenAccent, foregroundColor: Colors.black, visualDensity: VisualDensity.compact), child: const Text('使用', style: TextStyle(fontSize: 12)), ), ); } else { action = SizedBox( height: 30, child: FilledButton( onPressed: () => manager.downloadModel(item), style: FilledButton.styleFrom( backgroundColor: Colors.greenAccent, foregroundColor: Colors.black, visualDensity: VisualDensity.compact), child: Text(sameMode ? '使用' : '下载', style: const TextStyle(fontSize: 12)), ), ); } return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white10, borderRadius: BorderRadius.circular(10), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: Center(child: thumb)), const SizedBox(height: 6), Row( children: [ Expanded( child: Text(item.datasetName, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600)), ), Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( color: accent.withValues(alpha: 0.25), borderRadius: BorderRadius.circular(4), ), child: Text( variant == kVariantS ? '高识别' : '高性能', style: TextStyle(color: accent, fontSize: 9), ), ), ], ), const SizedBox(height: 2), Text('v${item.version}', style: const TextStyle(color: Colors.white38, fontSize: 10)), const SizedBox(height: 6), action, ], ), ); } }