Files
proxypin/lib/ui/content/web_socket.dart
wanghongenpin 435529cd3b desktop init
2025-11-24 17:06:12 +08:00

244 lines
8.8 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
import 'dart:math';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:proxypin/ui/component/utils.dart';
import 'package:proxypin/utils/lang.dart';
import 'package:proxypin/utils/num.dart';
import '../../l10n/app_localizations.dart';
import '../../network/http/http.dart';
import '../../network/http/websocket.dart';
import '../../utils/platform.dart';
import '../component/app_dialog.dart';
import '../component/json/json_text.dart';
import '../component/json/json_viewer.dart';
import '../component/json/theme.dart';
///以聊天对话框样式展示websocket消息
class Websocket extends StatelessWidget {
final ValueWrap<HttpRequest> request;
final ValueWrap<HttpResponse> response;
const Websocket(this.request, this.response, {super.key});
@override
Widget build(BuildContext context) {
AppLocalizations localizations = AppLocalizations.of(context)!;
var request = this.request.get();
if (request == null) {
return const SizedBox();
}
List<WebSocketFrame> messages = List.from(request.messages);
var response = this.response.get();
if (response != null) {
messages.addAll(response.messages);
}
messages.sort((a, b) => a.time.compareTo(b.time));
return ListView.builder(
padding: const EdgeInsets.only(bottom: 15),
itemCount: messages.length,
itemBuilder: (context, index) {
var message = messages[index];
var avatar = SelectionContainer.disabled(
child: CircleAvatar(
backgroundColor: message.isFromClient ? Colors.green : Colors.blue,
child:
Text(message.isFromClient ? 'C' : 'S', style: const TextStyle(fontSize: 18, color: Colors.white))));
var previewButton = IconButton(
tooltip: "Preview",
onPressed: () {
showDialog(context: context, builder: (context) => _PreviewDialog(bytes: message.payloadData));
},
icon: Icon(Icons.expand_more, color: ColorScheme.of(context).primary),
);
return Padding(
padding: const EdgeInsets.only(bottom: 5),
child: Row(
mainAxisAlignment: message.isFromClient ? MainAxisAlignment.start : MainAxisAlignment.end,
children: [
if (message.isFromClient) avatar,
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: message.isFromClient ? CrossAxisAlignment.start : CrossAxisAlignment.end,
children: [
SelectionContainer.disabled(
child:
Text(message.time.format(), style: const TextStyle(fontSize: 12, color: Colors.grey))),
Row(mainAxisSize: MainAxisSize.min, children: [
if (!message.isFromClient) previewButton,
Flexible(
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: message.isFromClient
? Colors.green.withOpacity(0.26)
: Colors.blue.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
),
child: SelectableText(
"${message.payloadDataAsString}${message.isBinary ? ' ${getPackage(message.payloadLength)}' : ''}",
maxLines: 3,
contextMenuBuilder: (context, editableTextState) =>
contextMenu(context, editableTextState,
customItem: ContextMenuButtonItem(
label: localizations.download,
onPressed: () async {
String? path = (await FilePicker.platform
.saveFile(fileName: "websocket.txt", bytes: message.payloadData));
if (path != null && context.mounted) {
CustomToast.success(localizations.saveSuccess).show(context);
}
},
type: ContextMenuButtonType.custom,
)),
)),
),
if (message.isFromClient) previewButton,
])
]),
),
const SizedBox(width: 8),
if (!message.isFromClient) avatar,
],
));
},
);
}
}
class _PreviewDialog extends StatefulWidget {
final List<int> bytes;
const _PreviewDialog({required this.bytes});
@override
State<_PreviewDialog> createState() => _PreviewDialogState();
}
class _PreviewDialogState extends State<_PreviewDialog> {
int tabIndex = 0; // 0: HEX, 1: TEXT
@override
Widget build(BuildContext context) {
var tabs = [
if (isJsonText(widget.bytes)) const Tab(text: "JSON Text"),
if (isJsonText(widget.bytes)) const Tab(text: "JSON"),
const Tab(text: "TEXT"),
const Tab(text: "HEX"),
];
return AlertDialog(
content: SizedBox(
width: min(MediaQuery.of(context).size.width * 0.8, 700),
height: min(MediaQuery.of(context).size.height * 0.6, 650),
child: DefaultTabController(
length: tabs.length,
initialIndex: tabIndex,
child: Column(mainAxisSize: MainAxisSize.min, children: [
TabBar(
tabs: tabs,
onTap: (index) {
setState(() {
tabIndex = index;
});
},
),
Expanded(
child: TabBarView(children: [
if (isJsonText(widget.bytes))
SingleChildScrollView(padding: const EdgeInsets.all(8.0), child: jsonText()),
if (isJsonText(widget.bytes))
SingleChildScrollView(padding: const EdgeInsets.all(8.0), child: jsonView()),
// TEXT
SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: SelectableText(safeTextPreview(widget.bytes)),
),
// HEX
SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: SelectableText(widget.bytes.map(intToHex).join(" ")),
),
]),
),
]),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(MaterialLocalizations.of(context).closeButtonLabel))
],
);
}
Widget jsonText() {
String body = utf8.decode(widget.bytes, allowMalformed: true);
dynamic jsonData;
try {
jsonData = json.decode(body);
} catch (e) {
jsonData = null;
}
if (jsonData == null) {
return SelectableText(safeTextPreview(widget.bytes));
}
return JsonText(json: jsonData, indent: Platforms.isDesktop() ? ' ' : ' ', colorTheme: ColorTheme.of(context));
}
Widget jsonView() {
String body = utf8.decode(widget.bytes, allowMalformed: true);
dynamic jsonData;
try {
jsonData = json.decode(body);
} catch (e) {
jsonData = null;
}
if (jsonData == null) {
return SelectableText(safeTextPreview(widget.bytes));
}
return JsonViewer(json.decode(body), colorTheme: ColorTheme.of(context));
}
//判断是否是json格式
bool isJsonText(List<int> bytes) {
return bytes.isNotEmpty && (bytes[0] == 0x7B || bytes[0] == 0x5B);
}
/// Format bytes as hex-dump string: 4 bytes per group (8 hex digits), space between groups, line break every 16 bytes
String formatHexDump(List<int> bytes) {
final buffer = StringBuffer();
// 每8个字节为一组用空格分隔组之间换行
for (int i = 0; i < bytes.length; i++) {
// 添加当前十六进制部分
buffer.write(bytes[i].toRadixString(16).padLeft(2, '0'));
if ((i + 1) % 2 == 0 && i != bytes.length - 1) {
buffer.write(' ');
}
}
return buffer.toString();
}
/// Decode bytes to string, non-printable as '.'
String safeTextPreview(List<int> bytes) {
try {
return utf8.decode(bytes);
} catch (_) {
return bytes.map((b) => b >= 32 && b <= 126 ? String.fromCharCode(b) : '.').join();
}
}
}