- BLE: 修复 BlueZ LocalName 与 Includes 冲突,串行注册与退避 - HTTP: 语音对话补 Live2D talking 标志与 session;Web 补 X-Session-Id - Flutter: 角色/对话/模型页 + API,Gradle/依赖升级,麦克风权限 - 文档: 新增 docs/STATUS.md,校准 CLAUDE/PROGRESS/README - 清理: 移除根目录截图与临时模型,运维脚本迁入 scripts/device
230 lines
9.0 KiB
Dart
230 lines
9.0 KiB
Dart
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);
|
||
}
|
||
}
|
||
}
|