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
This commit is contained in:
202
clients/flutter/lib/screens/characters_screen.dart
Normal file
202
clients/flutter/lib/screens/characters_screen.dart
Normal file
@@ -0,0 +1,202 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../models/character_pack.dart';
|
||||
import '../providers/character_provider.dart';
|
||||
import '../providers/chat_provider.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
|
||||
class CharactersScreen extends StatefulWidget {
|
||||
const CharactersScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CharactersScreen> createState() => _CharactersScreenState();
|
||||
}
|
||||
|
||||
class _CharactersScreenState extends State<CharactersScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<CharacterProvider>().refresh();
|
||||
});
|
||||
}
|
||||
|
||||
IconData _iconFor(CharacterPack pack) {
|
||||
switch (pack.iconHint) {
|
||||
case IconDataLike.person:
|
||||
return Icons.person_rounded;
|
||||
case IconDataLike.mic:
|
||||
return Icons.mic_rounded;
|
||||
case IconDataLike.sparkle:
|
||||
return Icons.auto_awesome_rounded;
|
||||
case IconDataLike.pets:
|
||||
return Icons.pets_rounded;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<CharacterProvider>();
|
||||
|
||||
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.md),
|
||||
child: Text(
|
||||
provider.error!,
|
||||
style: const TextStyle(color: AppColors.error),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'选择角色内容包,设备画面与对话人设会同步切换。',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
if (provider.isLoading && provider.packs.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 48),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else if (provider.packs.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 48),
|
||||
child: Center(child: Text('暂无角色配置包')),
|
||||
)
|
||||
else
|
||||
...provider.packs.map((pack) {
|
||||
final active = pack.filename == provider.activeFilename;
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: AppSpacing.md),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(AppRadius.large),
|
||||
onTap: provider.isSwitching || active
|
||||
? null
|
||||
: () async {
|
||||
final ok = await provider.switchTo(pack.filename);
|
||||
if (!context.mounted) return;
|
||||
if (ok) {
|
||||
// 切换角色清空对话上下文(PRD §2.4)
|
||||
context.read<ChatProvider>().clearHistory();
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
ok
|
||||
? '已切换到 ${pack.name}'
|
||||
: (provider.error ?? '切换失败'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundColor: active
|
||||
? AppColors.primary.withValues(alpha: 0.2)
|
||||
: AppColors.background,
|
||||
child: Icon(
|
||||
_iconFor(pack),
|
||||
color: active
|
||||
? AppColors.primary
|
||||
: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
pack.name,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium,
|
||||
),
|
||||
),
|
||||
if (active) ...[
|
||||
const SizedBox(width: 8),
|
||||
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(
|
||||
'${pack.typeLabel} · ${pack.renderType} · ${pack.filename}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (provider.isSwitching && !active)
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
else
|
||||
Icon(
|
||||
active
|
||||
? Icons.check_circle_rounded
|
||||
: Icons.chevron_right_rounded,
|
||||
color: active
|
||||
? AppColors.success
|
||||
: AppColors.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user