Files
ShowenV2/clients/flutter/lib/screens/models_screen.dart
XiuchengWu 6f7e24db63 feat: M2.1 客户端骨架、BLE 修复、仓库整理与现状板
- BLE: 修复 BlueZ LocalName 与 Includes 冲突,串行注册与退避
- HTTP: 语音对话补 Live2D talking 标志与 session;Web 补 X-Session-Id
- Flutter: 角色/对话/模型页 + API,Gradle/依赖升级,麦克风权限
- 文档: 新增 docs/STATUS.md,校准 CLAUDE/PROGRESS/README
- 清理: 移除根目录截图与临时模型,运维脚本迁入 scripts/device
2026-07-09 12:41:34 +08:00

230 lines
9.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/ai_model.dart';
import '../providers/model_provider.dart';
import '../theme/app_colors.dart';
class ModelsScreen extends StatefulWidget {
const ModelsScreen({super.key});
@override
State<ModelsScreen> createState() => _ModelsScreenState();
}
class _ModelsScreenState extends State<ModelsScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<ModelProvider>().refresh();
});
}
@override
Widget build(BuildContext context) {
final provider = context.watch<ModelProvider>();
final snapshot = provider.snapshot;
return Scaffold(
appBar: AppBar(
title: const Text('模型管理'),
actions: [
IconButton(
onPressed: provider.isLoading ? null : () => provider.refresh(),
icon: const Icon(Icons.refresh_rounded),
),
],
),
body: RefreshIndicator(
onRefresh: provider.refresh,
child: ListView(
padding: const EdgeInsets.all(AppSpacing.md),
children: [
if (provider.error != null)
Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: Text(
provider.error!,
style: const TextStyle(color: AppColors.error),
),
),
if (provider.status != null)
Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: Text(
provider.status!,
style: const TextStyle(color: AppColors.textSecondary),
),
),
if (snapshot != null) ...[
Card(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'存储 ${snapshot.usedLabel} / ${snapshot.quotaLabel}',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
LinearProgressIndicator(value: snapshot.usageRatio),
const SizedBox(height: 8),
Text(
'激活: LLM=${snapshot.active['llm'] ?? '-'} · '
'ASR=${snapshot.active['asr'] ?? '-'} · '
'TTS=${snapshot.active['tts'] ?? '-'}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppColors.textSecondary,
),
),
],
),
),
),
const SizedBox(height: AppSpacing.md),
],
if (provider.isLoading && snapshot == null)
const Padding(
padding: EdgeInsets.only(top: 48),
child: Center(child: CircularProgressIndicator()),
)
else if (snapshot == null || snapshot.models.isEmpty)
const Padding(
padding: EdgeInsets.only(top: 48),
child: Center(child: Text('暂无模型清单AI 插件可能未就绪)')),
)
else
...snapshot.models.map((model) {
final active = snapshot.isActive(model);
return Card(
margin: const EdgeInsets.only(bottom: AppSpacing.md),
child: Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
model.name,
style: Theme.of(context).textTheme.titleMedium,
),
),
if (active)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color:
AppColors.success.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(999),
),
child: const Text(
'使用中',
style: TextStyle(
color: AppColors.success,
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 4),
Text(
'${model.kindLabel} · ${model.sizeLabel} · 内存 ${model.memoryLabel}',
style:
Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppColors.textSecondary,
),
),
if (model.recommendedTier.isNotEmpty)
Text(
model.recommendedTier,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: AppColors.textSecondary),
),
if (!model.downloaded && model.downloadProgress > 0)
Padding(
padding: const EdgeInsets.only(top: 8),
child: LinearProgressIndicator(
value: model.downloadProgress / 100.0,
),
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
if (!model.downloaded)
FilledButton.tonal(
onPressed: provider.isActing
? null
: () => provider.download(model.id),
child: Text(
model.downloadProgress > 0 &&
model.downloadProgress < 100
? '下载中 ${model.downloadProgress}%'
: '下载',
),
),
if (model.downloaded && !active)
FilledButton(
onPressed: provider.isActing
? null
: () => provider.switchTo(
modelId: model.id,
kind: model.kind,
),
child: const Text('切换使用'),
),
if (model.downloaded && !active)
OutlinedButton(
onPressed: provider.isActing
? null
: () => _confirmDelete(context, model),
child: const Text('删除'),
),
],
),
],
),
),
);
}),
],
),
),
);
}
Future<void> _confirmDelete(BuildContext context, AiModelInfo model) async {
final ok = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('删除模型'),
content: Text('确定删除 ${model.name}?此操作不可恢复。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('删除'),
),
],
),
);
if (ok == true && context.mounted) {
await context.read<ModelProvider>().delete(model.id);
}
}
}