Files
ShowenV2/clients/flutter/lib/providers/device_provider.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

234 lines
7.0 KiB
Dart

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();
}
}