Files
ShowenV2/clients/flutter/lib/screens/network_screen.dart
showen bff9ec535d feat: Flutter 客户端 App + Web UI APK 下载入口
- 新增 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>
2026-03-14 02:09:52 +08:00

202 lines
7.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../providers/device_provider.dart';
import '../providers/wifi_provider.dart';
import '../theme/app_colors.dart';
import '../widgets/control_button.dart';
import '../widgets/status_card.dart';
import '../widgets/wifi_list_tile.dart';
class NetworkScreen extends StatefulWidget {
const NetworkScreen({super.key});
@override
State<NetworkScreen> createState() => _NetworkScreenState();
}
class _NetworkScreenState extends State<NetworkScreen> {
final TextEditingController _ssidController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _apSsidController = TextEditingController(text: 'showen');
final TextEditingController _apPasswordController = TextEditingController(text: '12345678');
@override
void dispose() {
_ssidController.dispose();
_passwordController.dispose();
_apSsidController.dispose();
_apPasswordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final wifiProvider = context.watch<WifiProvider>();
final deviceProvider = context.watch<DeviceProvider>();
final wifiStatus = wifiProvider.status;
final bleStatus = deviceProvider.status.bleStatus;
return Scaffold(
appBar: AppBar(title: const Text('网络设置')),
body: ListView(
padding: const EdgeInsets.all(AppSpacing.md),
children: [
StatusCard(
title: 'WiFi 状态',
value: wifiStatus.connected ? (wifiStatus.ssid ?? '已连接') : '未连接',
subtitle: wifiStatus.ip ?? '尚未获取 IP 地址',
icon: Icons.router_rounded,
accentColor: wifiStatus.connected ? AppColors.success : AppColors.warning,
),
const SizedBox(height: AppSpacing.md),
Card(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('连接 WiFi', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: AppSpacing.md),
TextField(
controller: _ssidController,
decoration: const InputDecoration(labelText: 'WiFi 名称'),
),
const SizedBox(height: AppSpacing.md),
TextField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(labelText: 'WiFi 密码'),
),
const SizedBox(height: AppSpacing.md),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: wifiProvider.isLoading ? null : _handleConnectWifi,
icon: const Icon(Icons.wifi_password_rounded),
label: const Text('连接当前 WiFi'),
),
),
],
),
),
),
const SizedBox(height: AppSpacing.lg),
Row(
children: [
Expanded(
child: ControlButton(
label: '扫描 WiFi',
icon: Icons.wifi_find_rounded,
onPressed: wifiProvider.isLoading
? null
: () => context.read<WifiProvider>().scanNetworks(),
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: ControlButton(
label: bleStatus?.running == true ? 'BLE 已就绪' : '启动 BLE',
icon: Icons.bluetooth_rounded,
isFilled: false,
onPressed: deviceProvider.isLoading
? null
: () => context.read<DeviceProvider>().startBle(deviceName: 'showen'),
),
),
],
),
const SizedBox(height: AppSpacing.lg),
Text('扫描结果', style: Theme.of(context).textTheme.headlineSmall),
const SizedBox(height: AppSpacing.md),
if (wifiProvider.networks.isEmpty)
const Card(
child: Padding(
padding: EdgeInsets.all(AppSpacing.md),
child: Text('暂无扫描结果'),
),
),
...wifiProvider.networks.map(
(network) => Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: WifiListTile(
network: network,
onTap: () {
_ssidController.text = network.ssid;
setState(() {});
},
),
),
),
const SizedBox(height: AppSpacing.lg),
Card(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text('热点开关', style: Theme.of(context).textTheme.titleMedium),
),
Switch(
value: wifiProvider.hotspotEnabled,
onChanged: (enabled) {
if (enabled) {
context.read<WifiProvider>().startHotspot(
ssid: _apSsidController.text.trim(),
password: _apPasswordController.text,
);
return;
}
context.read<WifiProvider>().stopHotspot();
},
),
],
),
const SizedBox(height: AppSpacing.sm),
TextField(
controller: _apSsidController,
decoration: const InputDecoration(labelText: '热点 SSID'),
),
const SizedBox(height: AppSpacing.md),
TextField(
controller: _apPasswordController,
obscureText: true,
decoration: const InputDecoration(labelText: '热点密码'),
),
],
),
),
),
const SizedBox(height: AppSpacing.md),
SizedBox(
width: double.infinity,
child: FilledButton.tonal(
onPressed: () => context.push('/network/ble-provision'),
child: const Text('进入 BLE 配网页面'),
),
),
],
),
);
}
void _handleConnectWifi() {
final ssid = _ssidController.text.trim();
if (ssid.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请输入 WiFi 名称')),
);
return;
}
context.read<WifiProvider>().connect(
ssid: ssid,
password: _passwordController.text,
);
}
}