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 _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; 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 initialize() async { await refresh(); await connect(); } Future refresh() async { _setLoading(true); 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; } catch (error) { _errorMessage = error.toString(); _status = _status.copyWith( connected: false, connectionType: 'offline', ipAddress: _deviceIp, ); } finally { _setLoading(false); } } Future loadDeviceOverview() => refresh(); Future connect() async { try { await _webSocketService.connect(_deviceIp); _webSocketConnected = _webSocketService.isConnected; _errorMessage = null; notifyListeners(); } catch (error) { _webSocketConnected = false; _errorMessage = error.toString(); notifyListeners(); } } Future 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 startBle({String? deviceName}) async { _setLoading(true); try { await _httpApiService.startBle(deviceName); await refresh(); } catch (error) { _errorMessage = error.toString(); notifyListeners(); } finally { _setLoading(false); } } Future 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 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) { final wifiStatus = WifiStatus.fromJson(payload); _status = _buildStatus( playerStatus: _status.playerStatus ?? PlayerStatus.initial(), wifiStatus: wifiStatus, bleStatus: _status.bleStatus ?? BleServiceStatus.initial(), ); notifyListeners(); } void _handleBleUpdate(Map 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 ?? '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(); } }