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>
This commit is contained in:
showen
2026-03-14 02:09:52 +08:00
parent d4f0eb7eca
commit bff9ec535d
45 changed files with 5903 additions and 75 deletions

View File

@@ -0,0 +1,196 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../models/ble_models.dart';
import '../services/ble_service.dart';
class BleProvider extends ChangeNotifier {
BleProvider({BleService? bleService}) : _bleService = bleService ?? BleService();
final BleService _bleService;
StreamSubscription<List<BleDevice>>? _scanSubscription;
StreamSubscription<BleStatus>? _statusSubscription;
List<BleDevice> _devices = const <BleDevice>[];
BleDevice? _selectedDevice;
BleStatus? _latestStatus;
ProvisioningState _provisioningState = ProvisioningState.scanning;
String? _errorMessage;
bool _isScanning = false;
bool _isConnecting = false;
bool _isProvisioning = false;
bool _isConnected = false;
bool _isDisposed = false;
List<BleDevice> get devices => _devices;
BleDevice? get selectedDevice => _selectedDevice;
BleStatus? get latestStatus => _latestStatus;
ProvisioningState get provisioningState => _provisioningState;
String? get errorMessage => _errorMessage;
bool get isScanning => _isScanning;
bool get isConnecting => _isConnecting;
bool get isProvisioning => _isProvisioning;
bool get isConnected => _isConnected;
Future<void> startScan() async {
_errorMessage = null;
_selectedDevice = null;
_isConnected = false;
_provisioningState = ProvisioningState.scanning;
_isScanning = true;
_notifySafely();
await _scanSubscription?.cancel();
_scanSubscription = _bleService
.scanForShowenDevices()
.listen((List<BleDevice> scannedDevices) {
_devices = scannedDevices;
_notifySafely();
}, onError: (Object error, StackTrace stackTrace) {
_errorMessage = error.toString();
_isScanning = false;
_provisioningState = ProvisioningState.failed;
_notifySafely();
});
Future<void>.delayed(const Duration(seconds: 6), () {
if (_isScanning) {
_isScanning = false;
_notifySafely();
}
});
}
Future<void> connectToDevice(BleDevice device) async {
_selectedDevice = device;
_errorMessage = null;
_isConnecting = true;
_isScanning = false;
_provisioningState = ProvisioningState.connecting;
_notifySafely();
try {
await _bleService.connectToDevice(device);
await _subscribeToStatus();
_isConnected = true;
} catch (error) {
_isConnected = false;
_errorMessage = error.toString();
_provisioningState = ProvisioningState.failed;
rethrow;
} finally {
_isConnecting = false;
_notifySafely();
}
}
Future<void> provisionWifi(String ssid, String password) async {
_errorMessage = null;
_latestStatus = null;
_isProvisioning = true;
_provisioningState = ProvisioningState.writingCredentials;
_notifySafely();
try {
final Future<BleStatus> operation = _bleService.provisionWifi(
ssid,
password,
timeout: const Duration(seconds: 30),
);
Future<void>.delayed(const Duration(milliseconds: 400), () {
if (_isProvisioning &&
_provisioningState == ProvisioningState.writingCredentials) {
_provisioningState = ProvisioningState.connectingWifi;
_notifySafely();
}
});
final BleStatus result = await operation;
_latestStatus = result;
_provisioningState = result.ok
? ProvisioningState.success
: ProvisioningState.failed;
if (!result.ok) {
_errorMessage = result.error ?? 'WiFi provisioning failed';
}
} on TimeoutException {
_errorMessage = 'BLE 配网超时30 秒)';
_provisioningState = ProvisioningState.failed;
rethrow;
} catch (error) {
_errorMessage = error.toString();
_provisioningState = ProvisioningState.failed;
rethrow;
} finally {
_isProvisioning = false;
_notifySafely();
}
}
Future<void> disconnect() async {
await _scanSubscription?.cancel();
await _statusSubscription?.cancel();
_scanSubscription = null;
_statusSubscription = null;
await _bleService.disconnect();
_isConnected = false;
_isConnecting = false;
_isProvisioning = false;
_selectedDevice = null;
_notifySafely();
}
Future<void> retryScan() async {
await disconnect();
_devices = const <BleDevice>[];
_latestStatus = null;
_errorMessage = null;
_provisioningState = ProvisioningState.scanning;
_notifySafely();
await startScan();
}
Future<void> _subscribeToStatus() async {
await _statusSubscription?.cancel();
final Stream<BleStatus> stream = await _bleService.subscribeToStatus();
_statusSubscription = stream.listen((BleStatus status) {
_latestStatus = status;
if (!status.ok) {
_errorMessage = status.error ?? 'BLE status returned an error';
}
if (status.action == 'connect') {
if (!status.ok) {
_provisioningState = ProvisioningState.failed;
} else if (!status.isQueued) {
_provisioningState = ProvisioningState.success;
} else if (_isProvisioning) {
_provisioningState = ProvisioningState.connectingWifi;
}
}
_notifySafely();
}, onError: (Object error, StackTrace stackTrace) {
_errorMessage = error.toString();
_provisioningState = ProvisioningState.failed;
_notifySafely();
});
}
@override
void dispose() {
_isDisposed = true;
unawaited(_scanSubscription?.cancel());
unawaited(_statusSubscription?.cancel());
unawaited(_bleService.dispose());
super.dispose();
}
void _notifySafely() {
if (_isDisposed) {
return;
}
notifyListeners();
}
}