M1.1 收尾: - 24项 P0/P1/P2 bug 修复 (Rust 107 tests + Flutter 15 tests) - Flutter App v0.3: cupertino_icons 修复, 单元测试, 调试面板, APK 52.6MB - 示例插件完善: manifest.json + 请求/响应示范 + 7个测试 - API 文档重写 (以 routes.rs 为唯一权威) - MILESTONES.md 更新至 100% M1.2 启动: - P0: 插件管理 API 闭环 (handle_manager_message Custom 分支 + broadcast_plugin_states) - ServiceManager 集成测试 8/8 (tests/m1_2_service_manager.rs) - M1.2 测试计划 (docs/M1.2_TEST_PLAN.md, 18个E2E场景) - 动态插件系统: auto_rollback + version_manager GC + 路径穿越防护 总计: Rust 115/115 测试, Flutter 15/15 测试, 零 warning Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
203 lines
7.3 KiB
Dart
203 lines
7.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../providers/player_provider.dart';
|
|
import '../theme/app_colors.dart';
|
|
|
|
class TriggerScreen extends StatefulWidget {
|
|
const TriggerScreen({super.key});
|
|
|
|
@override
|
|
State<TriggerScreen> createState() => _TriggerScreenState();
|
|
}
|
|
|
|
class _TriggerScreenState extends State<TriggerScreen> {
|
|
final TextEditingController _triggerController = TextEditingController();
|
|
final TextEditingController _valueController = TextEditingController();
|
|
String? _selectedScene;
|
|
|
|
static const List<_PresetTrigger> _presets = <_PresetTrigger>[
|
|
_PresetTrigger(label: '语音唤醒', name: 'wake', icon: Icons.mic_rounded),
|
|
_PresetTrigger(label: '按钮 1', name: 'button1', icon: Icons.filter_1_rounded),
|
|
_PresetTrigger(label: '按钮 2', name: 'button2', icon: Icons.filter_2_rounded),
|
|
_PresetTrigger(label: '触摸传感器', name: 'touch', icon: Icons.touch_app_rounded),
|
|
];
|
|
|
|
@override
|
|
void dispose() {
|
|
_triggerController.dispose();
|
|
_valueController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final provider = context.watch<PlayerProvider>();
|
|
_selectedScene ??= provider.sceneOptions.isNotEmpty ? provider.sceneOptions.first : null;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('状态机触发')),
|
|
body: RefreshIndicator(
|
|
onRefresh: _handleRefresh,
|
|
child: ListView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
padding: const EdgeInsets.all(AppSpacing.md),
|
|
children: [
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(AppSpacing.md),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('当前状态', style: Theme.of(context).textTheme.titleMedium),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
Text(
|
|
provider.currentState,
|
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
|
color: AppColors.primary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: AppSpacing.lg),
|
|
Text('预设触发器', style: Theme.of(context).textTheme.headlineSmall),
|
|
const SizedBox(height: AppSpacing.md),
|
|
GridView.count(
|
|
crossAxisCount: 2,
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
crossAxisSpacing: AppSpacing.md,
|
|
mainAxisSpacing: AppSpacing.md,
|
|
childAspectRatio: 1.35,
|
|
children: _presets.map((preset) {
|
|
return InkWell(
|
|
borderRadius: BorderRadius.circular(AppRadius.large),
|
|
onTap: provider.isLoading
|
|
? null
|
|
: () => context.read<PlayerProvider>().triggerEvent(preset.name),
|
|
child: Ink(
|
|
decoration: BoxDecoration(
|
|
color: AppColors.card,
|
|
borderRadius: BorderRadius.circular(AppRadius.large),
|
|
border: Border.all(color: AppColors.border),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(preset.icon, color: AppColors.accent, size: 32),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
Text(preset.label),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}).toList(growable: false),
|
|
),
|
|
const SizedBox(height: AppSpacing.lg),
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(AppSpacing.md),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('自定义触发器', style: Theme.of(context).textTheme.titleMedium),
|
|
const SizedBox(height: AppSpacing.md),
|
|
TextField(
|
|
controller: _triggerController,
|
|
decoration: const InputDecoration(labelText: '触发器名称'),
|
|
),
|
|
const SizedBox(height: AppSpacing.md),
|
|
TextField(
|
|
controller: _valueController,
|
|
decoration: const InputDecoration(labelText: '可选参数值'),
|
|
),
|
|
const SizedBox(height: AppSpacing.md),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: FilledButton.icon(
|
|
onPressed: provider.isLoading ? null : _handleCustomTrigger,
|
|
icon: const Icon(Icons.send_rounded),
|
|
label: const Text('发送触发器'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: AppSpacing.lg),
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(AppSpacing.md),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('场景切换', style: Theme.of(context).textTheme.titleMedium),
|
|
const SizedBox(height: AppSpacing.md),
|
|
DropdownButtonFormField<String>(
|
|
initialValue: provider.sceneOptions.contains(_selectedScene) ? _selectedScene : null,
|
|
items: provider.sceneOptions
|
|
.map(
|
|
(scene) => DropdownMenuItem<String>(
|
|
value: scene,
|
|
child: Text(scene),
|
|
),
|
|
)
|
|
.toList(growable: false),
|
|
onChanged: (value) => setState(() => _selectedScene = value),
|
|
decoration: const InputDecoration(labelText: '选择场景'),
|
|
),
|
|
const SizedBox(height: AppSpacing.md),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: FilledButton.tonal(
|
|
onPressed: provider.isLoading || _selectedScene == null
|
|
? null
|
|
: () => context.read<PlayerProvider>().switchScene(_selectedScene!),
|
|
child: const Text('切换场景'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _handleRefresh() {
|
|
return context.read<PlayerProvider>().bootstrap();
|
|
}
|
|
|
|
void _handleCustomTrigger() {
|
|
final name = _triggerController.text.trim();
|
|
final value = _valueController.text.trim();
|
|
if (name.isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('请输入触发器名称')),
|
|
);
|
|
return;
|
|
}
|
|
|
|
context.read<PlayerProvider>().triggerEvent(
|
|
name,
|
|
value: value.isEmpty ? null : value,
|
|
);
|
|
}
|
|
}
|
|
|
|
class _PresetTrigger {
|
|
const _PresetTrigger({
|
|
required this.label,
|
|
required this.name,
|
|
required this.icon,
|
|
});
|
|
|
|
final String label;
|
|
final String name;
|
|
final IconData icon;
|
|
}
|