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:
2026-07-09 12:41:34 +08:00
parent 7cb8cee70d
commit 6f7e24db63
51 changed files with 2674 additions and 351 deletions

View 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,
),
],
),
),
),
);
}),
],
),
),
);
}
}