319 lines
9.6 KiB
Dart
319 lines
9.6 KiB
Dart
import 'dart:async';
|
||
import 'dart:convert';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:gwid/api/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<CustomRequestScreen> createState() => _CustomRequestScreenState();
|
||
}
|
||
|
||
class _CustomRequestScreenState extends State<CustomRequestScreen> {
|
||
final _requestController = TextEditingController();
|
||
final _scrollController = ScrollController();
|
||
|
||
String? _response;
|
||
String? _error;
|
||
bool _isLoading = false;
|
||
|
||
final List<RequestHistoryItem> _history = [];
|
||
|
||
void _handleResponse(Map<String, dynamic> 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<void> _sendRequest() async {
|
||
if (_isLoading) return;
|
||
FocusScope.of(context).unfocus();
|
||
|
||
final requestText = _requestController.text.isEmpty
|
||
? '{}'
|
||
: _requestController.text;
|
||
Map<String, dynamic> requestJson;
|
||
|
||
try {
|
||
requestJson = jsonDecode(requestText) as Map<String, dynamic>;
|
||
} 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;
|
||
});
|
||
}
|
||
});
|
||
|
||
// If you're touching ( you doing this rignt now ) this file, STOP IMEDIATLY! This is violiating TeamKomet code revision policy!
|
||
// If you're touching ( you doing this rignt now ) this file, AND DONT WANT TO STOP! REFACTOR IT IMMEDIATLY TO MAKE SOME SENSE AND DONT LOOK LIKE SHIT BY AI
|
||
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();
|
||
}
|
||
}
|