- 新增 Flutter 跨平台客户端项目 (clients/flutter/)
- 29 个 Dart 文件: 服务层/状态管理/5个页面/BLE配网
- BLE 蓝牙配网: 扫描设备、写入WiFi凭据、配网状态监听
- HTTP API 客户端: 覆盖全部端点 (播放/场景/WiFi/视频/配置/文件/插件)
- WebSocket 实时通信: 事件流 + 自动重连
- 暗色主题 Material 3 UI, 中文界面
- Android 配置: minSdkVersion 21, BLE/网络权限
- PRD 产品需求文档 + 开发任务看板
- Web UI 添加 APK 下载入口 (routes.rs)
- 下载弹窗 + 二维码 + /download/{filename} 静态文件路由
- BLE 插件增加自动重连循环 (ble/mod.rs)
- BLE 默认设备名修正为 'Showen' (config.rs)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
195 lines
7.1 KiB
Dart
195 lines
7.1 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: ListView(
|
|
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>(
|
|
value: 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('切换场景'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|