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:
233
clients/flutter/lib/providers/device_provider.dart
Normal file
233
clients/flutter/lib/providers/device_provider.dart
Normal file
@@ -0,0 +1,233 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/ble_status.dart';
|
||||
import '../models/device_status.dart';
|
||||
import '../models/player_status.dart';
|
||||
import '../models/wifi_status.dart';
|
||||
import '../services/http_api_service.dart';
|
||||
import '../services/web_socket_service.dart';
|
||||
|
||||
class DeviceProvider extends ChangeNotifier {
|
||||
DeviceProvider({
|
||||
required HttpApiService httpApiService,
|
||||
required WebSocketService webSocketService,
|
||||
String initialDeviceIp = '127.0.0.1',
|
||||
}) : _httpApiService = httpApiService,
|
||||
_webSocketService = webSocketService,
|
||||
_deviceIp = _normalizeDeviceIp(initialDeviceIp) {
|
||||
_httpApiService.baseUrl = 'http://$_deviceIp:8080';
|
||||
_connectionSubscription = _webSocketService.onConnectionChanged.listen(
|
||||
_handleConnectionChanged,
|
||||
);
|
||||
_statusSubscription = _webSocketService.onStatusUpdate.listen(_handleStatusUpdate);
|
||||
_wifiSubscription = _webSocketService.onWifiUpdate.listen(_handleWifiUpdate);
|
||||
_bleSubscription = _webSocketService.onBleUpdate.listen(_handleBleUpdate);
|
||||
}
|
||||
|
||||
final HttpApiService _httpApiService;
|
||||
final WebSocketService _webSocketService;
|
||||
|
||||
late final StreamSubscription<SocketConnectionStatus> _connectionSubscription;
|
||||
late final StreamSubscription<Map<String, dynamic>> _statusSubscription;
|
||||
late final StreamSubscription<Map<String, dynamic>> _wifiSubscription;
|
||||
late final StreamSubscription<Map<String, dynamic>> _bleSubscription;
|
||||
|
||||
DeviceStatus _status = DeviceStatus.initial();
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
bool _webSocketConnected = false;
|
||||
String _deviceIp;
|
||||
|
||||
DeviceStatus get status => _status;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get errorMessage => _errorMessage;
|
||||
bool get webSocketConnected => _webSocketConnected;
|
||||
String get deviceIp => _deviceIp;
|
||||
HttpApiService get httpApiService => _httpApiService;
|
||||
|
||||
Future<void> initialize() async {
|
||||
await refresh();
|
||||
await connect();
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
_setLoading(true);
|
||||
try {
|
||||
final results = await Future.wait<dynamic>([
|
||||
_httpApiService.getPlaybackStatus(),
|
||||
_httpApiService.getWifiStatus(),
|
||||
_httpApiService.getBleStatus(),
|
||||
]);
|
||||
|
||||
_status = _buildStatus(
|
||||
playerStatus: results[0] as PlayerStatus,
|
||||
wifiStatus: results[1] as WifiStatus,
|
||||
bleStatus: results[2] as BleServiceStatus,
|
||||
);
|
||||
_errorMessage = null;
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
_status = _status.copyWith(
|
||||
connected: false,
|
||||
connectionType: 'offline',
|
||||
ipAddress: _deviceIp,
|
||||
);
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadDeviceOverview() => refresh();
|
||||
|
||||
Future<void> connect() async {
|
||||
try {
|
||||
await _webSocketService.connect(_deviceIp);
|
||||
_webSocketConnected = _webSocketService.isConnected;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
} catch (error) {
|
||||
_webSocketConnected = false;
|
||||
_errorMessage = error.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateDeviceIp(String ip) async {
|
||||
final normalized = _normalizeDeviceIp(ip);
|
||||
_deviceIp = normalized;
|
||||
_httpApiService.baseUrl = 'http://$_deviceIp:8080';
|
||||
_status = _status.copyWith(ipAddress: normalized, updatedAt: DateTime.now());
|
||||
notifyListeners();
|
||||
|
||||
await _webSocketService.disconnect();
|
||||
await initialize();
|
||||
}
|
||||
|
||||
Future<void> startBle({String? deviceName}) async {
|
||||
_setLoading(true);
|
||||
try {
|
||||
await _httpApiService.startBle(deviceName);
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
notifyListeners();
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopBle() async {
|
||||
_setLoading(true);
|
||||
try {
|
||||
await _httpApiService.stopBle();
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
notifyListeners();
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleConnectionChanged(SocketConnectionStatus connectionStatus) {
|
||||
_webSocketConnected = connectionStatus == SocketConnectionStatus.connected;
|
||||
if (!_webSocketConnected) {
|
||||
_status = _status.copyWith(connectionType: _status.connectionType);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleStatusUpdate(Map<String, dynamic> payload) {
|
||||
final playerStatus = PlayerStatus.fromJson(payload);
|
||||
_status = _buildStatus(
|
||||
playerStatus: playerStatus,
|
||||
wifiStatus: _status.wifiStatus ?? WifiStatus.disconnected(),
|
||||
bleStatus: _status.bleStatus ?? BleServiceStatus.initial(),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleWifiUpdate(Map<String, dynamic> payload) {
|
||||
final wifiStatus = WifiStatus.fromJson(payload);
|
||||
_status = _buildStatus(
|
||||
playerStatus: _status.playerStatus ?? PlayerStatus.initial(),
|
||||
wifiStatus: wifiStatus,
|
||||
bleStatus: _status.bleStatus ?? BleServiceStatus.initial(),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleBleUpdate(Map<String, dynamic> payload) {
|
||||
final normalized = <String, dynamic>{
|
||||
'running': payload['running'] ?? payload['ready'] ?? false,
|
||||
'embedded': payload['embedded'] ?? false,
|
||||
'device_name': payload['device_name'] ?? payload['name'],
|
||||
};
|
||||
final bleStatus = BleServiceStatus.fromJson(normalized);
|
||||
_status = _buildStatus(
|
||||
playerStatus: _status.playerStatus ?? PlayerStatus.initial(),
|
||||
wifiStatus: _status.wifiStatus ?? WifiStatus.disconnected(),
|
||||
bleStatus: bleStatus,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
DeviceStatus _buildStatus({
|
||||
required PlayerStatus playerStatus,
|
||||
required WifiStatus wifiStatus,
|
||||
required BleServiceStatus bleStatus,
|
||||
}) {
|
||||
final connectionType = wifiStatus.connected
|
||||
? 'wifi'
|
||||
: bleStatus.running
|
||||
? 'ble'
|
||||
: 'offline';
|
||||
|
||||
return DeviceStatus(
|
||||
connected: wifiStatus.connected || bleStatus.running || _webSocketConnected,
|
||||
connectionType: connectionType,
|
||||
deviceName: bleStatus.deviceName ?? 'ShowenV2',
|
||||
ipAddress: wifiStatus.ip ?? _deviceIp,
|
||||
playerStatus: playerStatus,
|
||||
wifiStatus: wifiStatus,
|
||||
bleStatus: bleStatus,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
void _setLoading(bool value) {
|
||||
_isLoading = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
static String _normalizeDeviceIp(String input) {
|
||||
var value = input.trim();
|
||||
if (value.startsWith('http://')) {
|
||||
value = value.substring(7);
|
||||
} else if (value.startsWith('https://')) {
|
||||
value = value.substring(8);
|
||||
} else if (value.startsWith('ws://')) {
|
||||
value = value.substring(5);
|
||||
}
|
||||
final slashIndex = value.indexOf('/');
|
||||
if (slashIndex >= 0) {
|
||||
value = value.substring(0, slashIndex);
|
||||
}
|
||||
final colonIndex = value.indexOf(':');
|
||||
if (colonIndex >= 0) {
|
||||
value = value.substring(0, colonIndex);
|
||||
}
|
||||
return value.isEmpty ? '127.0.0.1' : value;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_connectionSubscription.cancel());
|
||||
unawaited(_statusSubscription.cancel());
|
||||
unawaited(_wifiSubscription.cancel());
|
||||
unawaited(_bleSubscription.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user