import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:gwid/api_service.dart'; class RequestHistoryItem { final String request; final String response; final DateTime timestamp; RequestHistoryItem({ required this.request, required this.response, required this.timestamp, }); } class CustomRequestScreen extends StatefulWidget { const CustomRequestScreen({super.key}); @override State createState() => _CustomRequestScreenState(); } class _CustomRequestScreenState extends State { final _requestController = TextEditingController(); final _scrollController = ScrollController(); String? _response; String? _error; bool _isLoading = false; final List _history = []; void _handleResponse(Map message, String originalRequest) { const encoder = JsonEncoder.withIndent(' '); final formattedResponse = encoder.convert(message); if (!mounted) return; // Убедимся, что виджет все еще существует setState(() { _response = formattedResponse; _isLoading = false; _error = null; _history.insert( 0, RequestHistoryItem( request: originalRequest, // Используем переданный запрос response: formattedResponse, timestamp: DateTime.now(), ), ); }); } Future _sendRequest() async { if (_isLoading) return; FocusScope.of(context).unfocus(); final requestText = _requestController.text.isEmpty ? '{}' : _requestController.text; Map requestJson; try { requestJson = jsonDecode(requestText) as Map; } catch (e) { setState(() { _error = 'Ошибка: Невалидный JSON в запросе.\n$e'; }); return; } setState(() { _isLoading = true; _response = null; _error = null; }); StreamSubscription? subscription; Timer? timeoutTimer; try { final int sentSeq = ApiService.instance.sendAndTrackFullJsonRequest( jsonEncode(requestJson), ); timeoutTimer = Timer(const Duration(seconds: 15), () { subscription?.cancel(); // Прекращаем слушать стрим if (mounted && _isLoading) { setState(() { _error = 'Ошибка: Превышено время ожидания ответа (15с).'; _isLoading = false; }); } }); subscription = ApiService.instance.messages.listen((message) { if (message['seq'] == sentSeq) { timeoutTimer?.cancel(); // Отменяем таймер subscription?.cancel(); // Отменяем подписку _handleResponse(message, requestText); // Обрабатываем ответ } }); } catch (e) { timeoutTimer?.cancel(); subscription?.cancel(); if (mounted) { setState(() { _error = 'Ошибка при отправке запроса: $e'; _isLoading = false; }); } } } void _useHistoryItem(RequestHistoryItem item) { _requestController.text = item.request; setState(() { _response = item.response; _error = null; }); _scrollController.animateTo( 0, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Custom WebSocket Request')), body: SingleChildScrollView( controller: _scrollController, padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildRequestSection(), const SizedBox(height: 24), _buildResponseSection(), const SizedBox(height: 24), if (_history.isNotEmpty) _buildHistoryWidget(), ], ), ), ); } Widget _buildRequestSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Запрос к серверу', style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 12), TextField( controller: _requestController, decoration: const InputDecoration( border: OutlineInputBorder(), alignLabelWithHint: true, hintText: 'Введите полный JSON запроса...', ), keyboardType: TextInputType.multiline, maxLines: 12, style: const TextStyle(fontFamily: 'monospace', fontSize: 14.0), ), const SizedBox(height: 16), FilledButton.icon( onPressed: _isLoading ? null : _sendRequest, icon: _isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon(Icons.send), label: Text(_isLoading ? 'Ожидание...' : 'Отправить запрос'), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), minimumSize: const Size(double.infinity, 50), ), ), ], ); } Widget _buildResponseSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Ответ от сервера', style: Theme.of(context).textTheme.titleLarge, ), if (_response != null) IconButton( icon: const Icon(Icons.copy_all_outlined), tooltip: 'Скопировать ответ', onPressed: () { Clipboard.setData(ClipboardData(text: _response!)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Ответ скопирован в буфер обмена'), backgroundColor: Colors.green, ), ); }, ), ], ), const SizedBox(height: 12), Container( width: double.infinity, constraints: const BoxConstraints(minHeight: 150), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainer, borderRadius: BorderRadius.circular(8), border: Border.all( color: Theme.of(context).colorScheme.outline.withOpacity(0.5), ), ), child: _buildResponseContent(), ), ], ); } Widget _buildResponseContent() { if (_isLoading) { return const Center(child: CircularProgressIndicator()); } if (_error != null) { return SelectableText( _error!, style: TextStyle( color: Theme.of(context).colorScheme.error, fontFamily: 'monospace', ), ); } if (_response != null) { return SelectableText( _response!, style: const TextStyle(fontFamily: 'monospace', fontSize: 14), ); } return Center( child: Text( 'Здесь появится ответ от сервера...', style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), ), ); } Widget _buildHistoryWidget() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('История запросов', style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 12), ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: _history.length, itemBuilder: (context, index) { final item = _history[index]; String opcode = 'N/A'; try { final decoded = jsonDecode(item.request); opcode = decoded['opcode']?.toString() ?? 'N/A'; } catch (_) {} return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: CircleAvatar(child: Text(opcode)), title: Text( 'Request: ${item.request.replaceAll('\n', ' ').substring(0, (item.request.length > 50) ? 50 : item.request.length)}...', overflow: TextOverflow.ellipsis, style: const TextStyle(fontFamily: 'monospace'), ), subtitle: Text( '${item.timestamp.hour.toString().padLeft(2, '0')}:${item.timestamp.minute.toString().padLeft(2, '0')}:${item.timestamp.second.toString().padLeft(2, '0')}', ), onTap: () => _useHistoryItem(item), ), ); }, ), ], ); } @override void dispose() { _requestController.dispose(); _scrollController.dispose(); super.dispose(); } }