feat: M1.1 完成 + M1.2 启动 — 全量更新
M1.1 收尾: - 24项 P0/P1/P2 bug 修复 (Rust 107 tests + Flutter 15 tests) - Flutter App v0.3: cupertino_icons 修复, 单元测试, 调试面板, APK 52.6MB - 示例插件完善: manifest.json + 请求/响应示范 + 7个测试 - API 文档重写 (以 routes.rs 为唯一权威) - MILESTONES.md 更新至 100% M1.2 启动: - P0: 插件管理 API 闭环 (handle_manager_message Custom 分支 + broadcast_plugin_states) - ServiceManager 集成测试 8/8 (tests/m1_2_service_manager.rs) - M1.2 测试计划 (docs/M1.2_TEST_PLAN.md, 18个E2E场景) - 动态插件系统: auto_rollback + version_manager GC + 路径穿越防护 总计: Rust 115/115 测试, Flutter 15/15 测试, 零 warning Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,28 +6,43 @@ import '../models/ble_status.dart';
|
||||
import '../models/device_status.dart';
|
||||
import '../models/player_status.dart';
|
||||
import '../models/wifi_status.dart';
|
||||
import '../services/device_storage_service.dart';
|
||||
import '../services/http_api_service.dart';
|
||||
import '../services/web_socket_service.dart';
|
||||
import 'debug_provider.dart';
|
||||
|
||||
class DeviceProvider extends ChangeNotifier {
|
||||
DeviceProvider({
|
||||
required HttpApiService httpApiService,
|
||||
required WebSocketService webSocketService,
|
||||
required DeviceStorageService deviceStorageService,
|
||||
required DebugProvider debugProvider,
|
||||
String initialDeviceIp = '127.0.0.1',
|
||||
int initialDevicePort = 5000,
|
||||
String? initialDeviceName,
|
||||
}) : _httpApiService = httpApiService,
|
||||
_webSocketService = webSocketService,
|
||||
_deviceIp = _normalizeDeviceIp(initialDeviceIp) {
|
||||
_httpApiService.baseUrl = 'http://$_deviceIp:8080';
|
||||
_debugProvider = debugProvider,
|
||||
_deviceStorageService = deviceStorageService,
|
||||
_deviceIp = _normalizeDeviceIp(initialDeviceIp),
|
||||
_devicePort = _normalizePort(initialDevicePort),
|
||||
_deviceName = _normalizeDeviceName(initialDeviceName) {
|
||||
_httpApiService.baseUrl = _buildBaseUrl(_deviceIp, _devicePort);
|
||||
_connectionSubscription = _webSocketService.onConnectionChanged.listen(
|
||||
_handleConnectionChanged,
|
||||
);
|
||||
_statusSubscription = _webSocketService.onStatusUpdate.listen(_handleStatusUpdate);
|
||||
_wifiSubscription = _webSocketService.onWifiUpdate.listen(_handleWifiUpdate);
|
||||
_statusSubscription = _webSocketService.onStatusUpdate.listen(
|
||||
_handleStatusUpdate,
|
||||
);
|
||||
_wifiSubscription =
|
||||
_webSocketService.onWifiUpdate.listen(_handleWifiUpdate);
|
||||
_bleSubscription = _webSocketService.onBleUpdate.listen(_handleBleUpdate);
|
||||
}
|
||||
|
||||
final HttpApiService _httpApiService;
|
||||
final WebSocketService _webSocketService;
|
||||
final DebugProvider _debugProvider;
|
||||
final DeviceStorageService _deviceStorageService;
|
||||
|
||||
late final StreamSubscription<SocketConnectionStatus> _connectionSubscription;
|
||||
late final StreamSubscription<Map<String, dynamic>> _statusSubscription;
|
||||
@@ -39,21 +54,41 @@ class DeviceProvider extends ChangeNotifier {
|
||||
String? _errorMessage;
|
||||
bool _webSocketConnected = false;
|
||||
String _deviceIp;
|
||||
int _devicePort;
|
||||
String _deviceName;
|
||||
List<SavedDevice> _deviceList = const <SavedDevice>[];
|
||||
|
||||
DeviceStatus get status => _status;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get errorMessage => _errorMessage;
|
||||
bool get webSocketConnected => _webSocketConnected;
|
||||
String get deviceIp => _deviceIp;
|
||||
int get devicePort => _devicePort;
|
||||
List<SavedDevice> get deviceList =>
|
||||
List<SavedDevice>.unmodifiable(_deviceList);
|
||||
HttpApiService get httpApiService => _httpApiService;
|
||||
|
||||
Future<void> initialize() async {
|
||||
_debugProvider.addHttpLog(
|
||||
'Initialize device provider',
|
||||
details: <String, Object>{'device': '$_deviceIp:$_devicePort'},
|
||||
);
|
||||
final lastDevice = await _deviceStorageService.getLastDevice();
|
||||
if (lastDevice != null) {
|
||||
_applyDevice(
|
||||
ip: lastDevice.ip, port: lastDevice.port, name: lastDevice.name);
|
||||
}
|
||||
await _refreshDeviceList();
|
||||
await refresh();
|
||||
await connect();
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
_setLoading(true);
|
||||
_debugProvider.addHttpLog(
|
||||
'Refresh device overview',
|
||||
details: <String, Object>{'device': '$_deviceIp:$_devicePort'},
|
||||
);
|
||||
try {
|
||||
final results = await Future.wait<dynamic>([
|
||||
_httpApiService.getPlaybackStatus(),
|
||||
@@ -67,12 +102,18 @@ class DeviceProvider extends ChangeNotifier {
|
||||
bleStatus: results[2] as BleServiceStatus,
|
||||
);
|
||||
_errorMessage = null;
|
||||
_debugProvider.addHttpLog('Device overview refreshed');
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
_debugProvider.addHttpLog(
|
||||
'Refresh device overview failed',
|
||||
details: error,
|
||||
);
|
||||
_status = _status.copyWith(
|
||||
connected: false,
|
||||
connectionType: 'offline',
|
||||
ipAddress: _deviceIp,
|
||||
deviceName: _deviceName,
|
||||
ipAddress: '$_deviceIp:$_devicePort',
|
||||
);
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
@@ -82,36 +123,98 @@ class DeviceProvider extends ChangeNotifier {
|
||||
Future<void> loadDeviceOverview() => refresh();
|
||||
|
||||
Future<void> connect() async {
|
||||
_debugProvider.addWsLog(
|
||||
'Connect WebSocket',
|
||||
details: <String, Object>{'device': '$_deviceIp:$_devicePort'},
|
||||
);
|
||||
try {
|
||||
await _webSocketService.connect(_deviceIp);
|
||||
await _webSocketService.connect(_deviceIp, port: _devicePort);
|
||||
_webSocketConnected = _webSocketService.isConnected;
|
||||
_errorMessage = null;
|
||||
_debugProvider.addWsLog('WebSocket connect request completed');
|
||||
notifyListeners();
|
||||
} catch (error) {
|
||||
_webSocketConnected = false;
|
||||
_errorMessage = error.toString();
|
||||
_debugProvider.addWsLog('WebSocket connect failed', details: error);
|
||||
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();
|
||||
Future<void> switchDevice(String input, {String? name}) async {
|
||||
final nextDevice = _parseDeviceInput(input, fallbackPort: _devicePort);
|
||||
final nextName =
|
||||
_normalizeDeviceName(name ?? _status.deviceName ?? _deviceName);
|
||||
_setLoading(true);
|
||||
_debugProvider.addHttpLog(
|
||||
'Switch device to ${nextDevice.ip}:${nextDevice.port}',
|
||||
details: <String, Object>{'name': nextName},
|
||||
);
|
||||
try {
|
||||
await _validateDeviceReachable(nextDevice.ip, nextDevice.port);
|
||||
|
||||
await _webSocketService.disconnect();
|
||||
await initialize();
|
||||
await _webSocketService.disconnect();
|
||||
_applyDevice(ip: nextDevice.ip, port: nextDevice.port, name: nextName);
|
||||
notifyListeners();
|
||||
|
||||
await _deviceStorageService.saveDevice(
|
||||
nextDevice.ip, nextDevice.port, nextName);
|
||||
await _refreshDeviceList();
|
||||
await refresh();
|
||||
await connect();
|
||||
|
||||
final resolvedName = _normalizeDeviceName(_status.deviceName ?? nextName);
|
||||
if (resolvedName != nextName) {
|
||||
_deviceName = resolvedName;
|
||||
await _deviceStorageService.saveDevice(
|
||||
nextDevice.ip,
|
||||
nextDevice.port,
|
||||
resolvedName,
|
||||
);
|
||||
await _refreshDeviceList();
|
||||
notifyListeners();
|
||||
}
|
||||
_debugProvider.addHttpLog(
|
||||
'Device switched successfully',
|
||||
details: <String, Object>{'device': '${nextDevice.ip}:${nextDevice.port}'},
|
||||
);
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
_debugProvider.addHttpLog('Switch device failed', details: error);
|
||||
notifyListeners();
|
||||
rethrow;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateDeviceIp(String ip) async {
|
||||
await switchDevice(ip);
|
||||
}
|
||||
|
||||
Future<void> removeStoredDevice(SavedDevice device) async {
|
||||
await _deviceStorageService.removeDevice(device.ip, device.port);
|
||||
await _refreshDeviceList();
|
||||
_debugProvider.addHttpLog(
|
||||
'Removed saved device ${device.address}',
|
||||
details: <String, Object>{'name': device.name},
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> startBle({String? deviceName}) async {
|
||||
_setLoading(true);
|
||||
_debugProvider.addHttpLog(
|
||||
'Start BLE service',
|
||||
details: <String, Object?>{'deviceName': deviceName},
|
||||
);
|
||||
try {
|
||||
await _httpApiService.startBle(deviceName);
|
||||
await refresh();
|
||||
_debugProvider.addHttpLog('BLE service started');
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
_debugProvider.addHttpLog('Start BLE service failed', details: error);
|
||||
notifyListeners();
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
@@ -120,11 +223,14 @@ class DeviceProvider extends ChangeNotifier {
|
||||
|
||||
Future<void> stopBle() async {
|
||||
_setLoading(true);
|
||||
_debugProvider.addHttpLog('Stop BLE service');
|
||||
try {
|
||||
await _httpApiService.stopBle();
|
||||
await refresh();
|
||||
_debugProvider.addHttpLog('BLE service stopped');
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
_debugProvider.addHttpLog('Stop BLE service failed', details: error);
|
||||
notifyListeners();
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
@@ -133,6 +239,9 @@ class DeviceProvider extends ChangeNotifier {
|
||||
|
||||
void _handleConnectionChanged(SocketConnectionStatus connectionStatus) {
|
||||
_webSocketConnected = connectionStatus == SocketConnectionStatus.connected;
|
||||
_debugProvider.addWsLog(
|
||||
'Device provider connection state: ${connectionStatus.name}',
|
||||
);
|
||||
if (!_webSocketConnected) {
|
||||
_status = _status.copyWith(connectionType: _status.connectionType);
|
||||
}
|
||||
@@ -140,6 +249,7 @@ class DeviceProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _handleStatusUpdate(Map<String, dynamic> payload) {
|
||||
_debugProvider.addWsLog('Received status update', details: payload);
|
||||
final playerStatus = PlayerStatus.fromJson(payload);
|
||||
_status = _buildStatus(
|
||||
playerStatus: playerStatus,
|
||||
@@ -150,6 +260,7 @@ class DeviceProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _handleWifiUpdate(Map<String, dynamic> payload) {
|
||||
_debugProvider.addWsLog('Received wifi update', details: payload);
|
||||
final wifiStatus = WifiStatus.fromJson(payload);
|
||||
_status = _buildStatus(
|
||||
playerStatus: _status.playerStatus ?? PlayerStatus.initial(),
|
||||
@@ -160,6 +271,7 @@ class DeviceProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _handleBleUpdate(Map<String, dynamic> payload) {
|
||||
_debugProvider.addBleLog('Received BLE update', details: payload);
|
||||
final normalized = <String, dynamic>{
|
||||
'running': payload['running'] ?? payload['ready'] ?? false,
|
||||
'embedded': payload['embedded'] ?? false,
|
||||
@@ -186,10 +298,11 @@ class DeviceProvider extends ChangeNotifier {
|
||||
: 'offline';
|
||||
|
||||
return DeviceStatus(
|
||||
connected: wifiStatus.connected || bleStatus.running || _webSocketConnected,
|
||||
connected:
|
||||
wifiStatus.connected || bleStatus.running || _webSocketConnected,
|
||||
connectionType: connectionType,
|
||||
deviceName: bleStatus.deviceName ?? 'ShowenV2',
|
||||
ipAddress: wifiStatus.ip ?? _deviceIp,
|
||||
deviceName: bleStatus.deviceName ?? _deviceName,
|
||||
ipAddress: wifiStatus.ip ?? '$_deviceIp:$_devicePort',
|
||||
playerStatus: playerStatus,
|
||||
wifiStatus: wifiStatus,
|
||||
bleStatus: bleStatus,
|
||||
@@ -197,11 +310,46 @@ class DeviceProvider extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _refreshDeviceList() async {
|
||||
_deviceList = await _deviceStorageService.getDevices();
|
||||
}
|
||||
|
||||
Future<void> _validateDeviceReachable(String ip, int port) async {
|
||||
final probeService = HttpApiService(baseUrl: _buildBaseUrl(ip, port));
|
||||
try {
|
||||
await probeService.getStatus().timeout(const Duration(seconds: 3));
|
||||
} on TimeoutException {
|
||||
throw Exception('设备不可达:连接超时(3 秒)');
|
||||
} on Exception catch (error) {
|
||||
throw Exception('设备不可达:$error');
|
||||
} finally {
|
||||
probeService.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
void _applyDevice({
|
||||
required String ip,
|
||||
required int port,
|
||||
String? name,
|
||||
}) {
|
||||
_deviceIp = _normalizeDeviceIp(ip);
|
||||
_devicePort = _normalizePort(port);
|
||||
_deviceName = _normalizeDeviceName(name);
|
||||
_httpApiService.baseUrl = _buildBaseUrl(_deviceIp, _devicePort);
|
||||
_status = _status.copyWith(
|
||||
deviceName: _deviceName,
|
||||
ipAddress: '$_deviceIp:$_devicePort',
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
void _setLoading(bool value) {
|
||||
_isLoading = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
static String _buildBaseUrl(String ip, int port) => 'http://$ip:$port';
|
||||
|
||||
static String _normalizeDeviceIp(String input) {
|
||||
var value = input.trim();
|
||||
if (value.startsWith('http://')) {
|
||||
@@ -210,6 +358,8 @@ class DeviceProvider extends ChangeNotifier {
|
||||
value = value.substring(8);
|
||||
} else if (value.startsWith('ws://')) {
|
||||
value = value.substring(5);
|
||||
} else if (value.startsWith('wss://')) {
|
||||
value = value.substring(6);
|
||||
}
|
||||
final slashIndex = value.indexOf('/');
|
||||
if (slashIndex >= 0) {
|
||||
@@ -222,6 +372,53 @@ class DeviceProvider extends ChangeNotifier {
|
||||
return value.isEmpty ? '127.0.0.1' : value;
|
||||
}
|
||||
|
||||
static int _normalizePort(int input) {
|
||||
if (input <= 0 || input > 65535) {
|
||||
return 5000;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
static String _normalizeDeviceName(String? input) {
|
||||
final value = input?.trim() ?? '';
|
||||
return value.isEmpty ? 'Showen' : value;
|
||||
}
|
||||
|
||||
static SavedDevice _parseDeviceInput(String input,
|
||||
{int fallbackPort = 5000}) {
|
||||
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);
|
||||
} else if (value.startsWith('wss://')) {
|
||||
value = value.substring(6);
|
||||
}
|
||||
|
||||
final slashIndex = value.indexOf('/');
|
||||
if (slashIndex >= 0) {
|
||||
value = value.substring(0, slashIndex);
|
||||
}
|
||||
|
||||
final colonIndex = value.lastIndexOf(':');
|
||||
final hasPort = colonIndex > 0 && colonIndex < value.length - 1;
|
||||
final ip =
|
||||
_normalizeDeviceIp(hasPort ? value.substring(0, colonIndex) : value);
|
||||
final port = hasPort
|
||||
? _normalizePort(
|
||||
int.tryParse(value.substring(colonIndex + 1)) ?? fallbackPort)
|
||||
: _normalizePort(fallbackPort);
|
||||
|
||||
return SavedDevice(
|
||||
ip: ip,
|
||||
port: port,
|
||||
name: 'Showen',
|
||||
lastUsedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_connectionSubscription.cancel());
|
||||
|
||||
Reference in New Issue
Block a user