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/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, _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); _bleSubscription = _webSocketService.onBleUpdate.listen(_handleBleUpdate); } final HttpApiService _httpApiService; final WebSocketService _webSocketService; final DebugProvider _debugProvider; final DeviceStorageService _deviceStorageService; late final StreamSubscription _connectionSubscription; late final StreamSubscription> _statusSubscription; late final StreamSubscription> _wifiSubscription; late final StreamSubscription> _bleSubscription; DeviceStatus _status = DeviceStatus.initial(); bool _isLoading = false; String? _errorMessage; bool _webSocketConnected = false; String _deviceIp; int _devicePort; String _deviceName; List _deviceList = const []; 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 get deviceList => List.unmodifiable(_deviceList); HttpApiService get httpApiService => _httpApiService; Future initialize() async { _debugProvider.addHttpLog( 'Initialize device provider', details: {'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 refresh() async { _setLoading(true); _debugProvider.addHttpLog( 'Refresh device overview', details: {'device': '$_deviceIp:$_devicePort'}, ); try { final results = await Future.wait([ _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; _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', deviceName: _deviceName, ipAddress: '$_deviceIp:$_devicePort', ); } finally { _setLoading(false); } } Future loadDeviceOverview() => refresh(); Future connect() async { _debugProvider.addWsLog( 'Connect WebSocket', details: {'device': '$_deviceIp:$_devicePort'}, ); try { 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 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: {'name': nextName}, ); try { await _validateDeviceReachable(nextDevice.ip, nextDevice.port); 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: {'device': '${nextDevice.ip}:${nextDevice.port}'}, ); } catch (error) { _errorMessage = error.toString(); _debugProvider.addHttpLog('Switch device failed', details: error); notifyListeners(); rethrow; } finally { _setLoading(false); } } Future updateDeviceIp(String ip) async { await switchDevice(ip); } Future removeStoredDevice(SavedDevice device) async { await _deviceStorageService.removeDevice(device.ip, device.port); await _refreshDeviceList(); _debugProvider.addHttpLog( 'Removed saved device ${device.address}', details: {'name': device.name}, ); notifyListeners(); } Future startBle({String? deviceName}) async { _setLoading(true); _debugProvider.addHttpLog( 'Start BLE service', details: {'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); } } Future 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); } } void _handleConnectionChanged(SocketConnectionStatus connectionStatus) { _webSocketConnected = connectionStatus == SocketConnectionStatus.connected; _debugProvider.addWsLog( 'Device provider connection state: ${connectionStatus.name}', ); if (!_webSocketConnected) { _status = _status.copyWith(connectionType: _status.connectionType); } notifyListeners(); } void _handleStatusUpdate(Map payload) { _debugProvider.addWsLog('Received status update', details: payload); final playerStatus = PlayerStatus.fromJson(payload); _status = _buildStatus( playerStatus: playerStatus, wifiStatus: _status.wifiStatus ?? WifiStatus.disconnected(), bleStatus: _status.bleStatus ?? BleServiceStatus.initial(), ); notifyListeners(); } void _handleWifiUpdate(Map payload) { _debugProvider.addWsLog('Received wifi update', details: payload); final wifiStatus = WifiStatus.fromJson(payload); _status = _buildStatus( playerStatus: _status.playerStatus ?? PlayerStatus.initial(), wifiStatus: wifiStatus, bleStatus: _status.bleStatus ?? BleServiceStatus.initial(), ); notifyListeners(); } void _handleBleUpdate(Map payload) { _debugProvider.addBleLog('Received BLE update', details: payload); final normalized = { '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 ?? _deviceName, ipAddress: wifiStatus.ip ?? '$_deviceIp:$_devicePort', playerStatus: playerStatus, wifiStatus: wifiStatus, bleStatus: bleStatus, updatedAt: DateTime.now(), ); } Future _refreshDeviceList() async { _deviceList = await _deviceStorageService.getDevices(); } Future _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://')) { 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.indexOf(':'); if (colonIndex >= 0) { value = value.substring(0, colonIndex); } 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()); unawaited(_statusSubscription.cancel()); unawaited(_wifiSubscription.cancel()); unawaited(_bleSubscription.cancel()); super.dispose(); } }