import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import '../models/app_event.dart'; import '../services/web_socket_service.dart'; enum DebugLogType { ble, ws, http } enum DebugLogFilter { all, ble, ws, http } class DebugLogEntry { const DebugLogEntry({ required this.timestamp, required this.type, required this.summary, this.details, }); final DateTime timestamp; final DebugLogType type; final String summary; final String? details; String get label { switch (type) { case DebugLogType.ble: return 'BLE'; case DebugLogType.ws: return 'WS'; case DebugLogType.http: return 'HTTP'; } } } class DebugProvider extends ChangeNotifier { DebugProvider({required WebSocketService webSocketService}) { _eventSubscription = webSocketService.events.listen(_handleEvent); _connectionSubscription = webSocketService.connectionStateStream.listen( _handleConnectionState, ); } static const int maxEntries = 500; final List _entries = []; late final StreamSubscription _eventSubscription; late final StreamSubscription _connectionSubscription; DebugLogFilter _filter = DebugLogFilter.all; List get entries => List.unmodifiable(_entries); DebugLogFilter get filter => _filter; List get filteredEntries { switch (_filter) { case DebugLogFilter.all: return entries.reversed.toList(growable: false); case DebugLogFilter.ble: return _entries .where((entry) => entry.type == DebugLogType.ble) .toList(growable: false) .reversed .toList(growable: false); case DebugLogFilter.ws: return _entries .where((entry) => entry.type == DebugLogType.ws) .toList(growable: false) .reversed .toList(growable: false); case DebugLogFilter.http: return _entries .where((entry) => entry.type == DebugLogType.http) .toList(growable: false) .reversed .toList(growable: false); } } void setFilter(DebugLogFilter value) { if (_filter == value) { return; } _filter = value; notifyListeners(); } void clearLogs() { if (_entries.isEmpty) { return; } _entries.clear(); notifyListeners(); } void addHttpLog(String summary, {Object? details}) { _addEntry(DebugLogType.http, summary, details: details); } void addBleLog(String summary, {Object? details}) { _addEntry(DebugLogType.ble, summary, details: details); } void addWsLog(String summary, {Object? details}) { _addEntry(DebugLogType.ws, summary, details: details); } void _handleEvent(AppEvent event) { final logType = _inferType(event); _addEntry( logType, _buildEventSummary(event, logType), details: { 'type': event.type, 'payload': event.payload, }, notify: true, ); } void _handleConnectionState(WsConnectionState state) { final String summary; switch (state) { case WsConnectionState.connected: summary = 'WebSocket connected'; break; case WsConnectionState.connecting: summary = 'WebSocket reconnecting'; break; case WsConnectionState.disconnected: summary = 'WebSocket disconnected'; break; } _addEntry(DebugLogType.ws, summary, notify: true); } DebugLogType _inferType(AppEvent event) { if (event.type.contains('ble')) { return DebugLogType.ble; } return DebugLogType.ws; } String _buildEventSummary(AppEvent event, DebugLogType type) { final payload = event.payload; switch (event.type) { case 'ble_update': final ready = payload['ready'] ?? payload['running']; final name = payload['device_name'] ?? payload['name']; return 'BLE update${name != null ? ' - $name' : ''}${ready != null ? ' (ready: $ready)' : ''}'; case 'wifi_update': final ssid = payload['ssid']?.toString(); final connected = payload['connected']; return 'WS wifi update${ssid != null && ssid.isNotEmpty ? ' - $ssid' : ''}${connected != null ? ' (connected: $connected)' : ''}'; case 'status_update': final current = payload['current_video']?.toString(); final running = payload['running']; return 'WS playback status${current != null && current.isNotEmpty ? ' - $current' : ''}${running != null ? ' (running: $running)' : ''}'; case 'state_update': final previous = payload['old_state']?.toString(); final next = payload['new_state']?.toString(); return 'WS state change${previous != null ? ' $previous ->' : ''} ${next ?? 'unknown'}'.trim(); case 'config_update': return 'WS config update'; default: final prefix = type == DebugLogType.ble ? 'BLE' : 'WS'; final compactPayload = _stringify(details: payload); return '$prefix ${event.type}: $compactPayload'; } } void _addEntry( DebugLogType type, String summary, { Object? details, bool notify = true, }) { _entries.add( DebugLogEntry( timestamp: DateTime.now(), type: type, summary: summary, details: details == null ? null : _stringify(details: details), ), ); if (_entries.length > maxEntries) { _entries.removeRange(0, _entries.length - maxEntries); } if (notify) { notifyListeners(); } } String _stringify({required Object details}) { try { final encoded = details is String ? details : jsonEncode(details); return encoded.length > 220 ? '${encoded.substring(0, 217)}...' : encoded; } catch (_) { final value = details.toString(); return value.length > 220 ? '${value.substring(0, 217)}...' : value; } } @override void dispose() { unawaited(_eventSubscription.cancel()); unawaited(_connectionSubscription.cancel()); super.dispose(); } }