import 'package:flutter/material.dart'; import '../models/model_manager.dart'; /// 设置弹层「模型清单」区块:2 列封面缩略图网格。 /// 未下载 →「使用」点击后显示下载进度,完成自动激活;已下载未激活 → 直接激活; /// 已激活 → 再次点击取消;下载失败 → 失败提示 + 重试。 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, _) { final items = manager.catalog; return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ 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), ), ], ); }, ); } } class _ModelCard extends StatelessWidget { final ModelCatalogItem item; final ModelManager manager; const _ModelCard({required this.item, required this.manager}); @override Widget build(BuildContext context) { final active = manager.isActive(item.datasetId); final downloaded = manager.isDownloaded(item.datasetId); final progress = manager.progressOf(item.datasetId); final error = manager.errorOf(item.datasetId); final thumb = ClipRRect( borderRadius: BorderRadius.circular(8), child: AspectRatio( aspectRatio: 4 / 3, child: 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), ), ), ), ); 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), 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) { action = SizedBox( height: 30, child: OutlinedButton( onPressed: () => manager.setActive(item.datasetId, false), style: OutlinedButton.styleFrom( foregroundColor: Colors.greenAccent, side: const BorderSide(color: Colors.greenAccent)), child: const Text('已使用', style: TextStyle(fontSize: 12)), ), ); } else if (downloaded) { action = SizedBox( height: 30, child: FilledButton( onPressed: () => manager.setActive(item.datasetId, 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: const Text('使用', style: 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), Text(item.datasetName, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600)), const SizedBox(height: 2), Text('v${item.version}', style: const TextStyle(color: Colors.white38, fontSize: 10)), const SizedBox(height: 6), action, ], ), ); } }