mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-04-25 22:29:49 +08:00
add export functionality for domain HAR files (#725)
This commit is contained in:
@@ -168,6 +168,7 @@
|
||||
"editRequest": "Edit and Request",
|
||||
"reSendRequest": "The request has been resent",
|
||||
"viewExport": "View Export",
|
||||
"exportDomainHar": "Export This Domain HAR",
|
||||
"timeDesc": "Descending by time",
|
||||
"timeAsc": "Ascending by time",
|
||||
|
||||
|
||||
@@ -1068,6 +1068,12 @@ abstract class AppLocalizations {
|
||||
/// **'View Export'**
|
||||
String get viewExport;
|
||||
|
||||
/// No description provided for @exportDomainHar.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Export This Domain HAR'**
|
||||
String get exportDomainHar;
|
||||
|
||||
/// No description provided for @timeDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -499,6 +499,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get viewExport => 'View Export';
|
||||
|
||||
@override
|
||||
String get exportDomainHar => 'Export This Domain HAR';
|
||||
|
||||
@override
|
||||
String get timeDesc => 'Descending by time';
|
||||
|
||||
|
||||
@@ -498,6 +498,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get viewExport => '视图导出';
|
||||
|
||||
@override
|
||||
String get exportDomainHar => '导出该域名 HAR';
|
||||
|
||||
@override
|
||||
String get timeDesc => '按时间降序';
|
||||
|
||||
@@ -1562,6 +1565,9 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
|
||||
@override
|
||||
String get viewExport => '檢視匯出';
|
||||
|
||||
@override
|
||||
String get exportDomainHar => '匯出該網域名稱 HAR';
|
||||
|
||||
@override
|
||||
String get timeDesc => '按時間降序';
|
||||
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
"editRequest": "编辑请求",
|
||||
"reSendRequest": "已重新发送请求",
|
||||
"viewExport": "视图导出",
|
||||
"exportDomainHar": "导出该域名 HAR",
|
||||
"timeDesc": "按时间降序",
|
||||
"timeAsc": "按时间升序",
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
"editRequest": "編輯請求",
|
||||
"reSendRequest": "已重新傳送請求",
|
||||
"viewExport": "檢視匯出",
|
||||
"exportDomainHar": "匯出該網域名稱 HAR",
|
||||
"timeDesc": "按時間降序",
|
||||
"timeAsc": "按時間升序",
|
||||
"search": "搜尋",
|
||||
|
||||
@@ -78,7 +78,7 @@ class HistoryStorage {
|
||||
return _histories.source;
|
||||
}
|
||||
|
||||
addListener(ListenerListEvent<HistoryItem> listener) async {
|
||||
void addListener(ListenerListEvent<HistoryItem> listener) {
|
||||
_histories.addListener(listener);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_desktop_context_menu/flutter_desktop_context_menu.dart';
|
||||
@@ -34,7 +35,9 @@ import 'package:proxypin/ui/component/transition.dart';
|
||||
import 'package:proxypin/ui/component/utils.dart';
|
||||
import 'package:proxypin/ui/content/panel.dart';
|
||||
import 'package:proxypin/ui/desktop/request/request.dart';
|
||||
import 'package:proxypin/utils/har.dart';
|
||||
import 'package:proxypin/utils/keyword_highlight.dart';
|
||||
import 'package:proxypin/utils/lang.dart';
|
||||
import 'package:proxypin/utils/listenable_list.dart';
|
||||
|
||||
import '../../component/model/search_model.dart';
|
||||
@@ -79,6 +82,8 @@ class DomainWidgetState extends State<DomainList> with AutomaticKeepAliveClientM
|
||||
|
||||
bool sortDesc = true;
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
|
||||
void changeState() {
|
||||
if (!changing) {
|
||||
changing = true;
|
||||
@@ -198,6 +203,7 @@ class DomainWidgetState extends State<DomainList> with AutomaticKeepAliveClientM
|
||||
proxyServer: widget.proxyServer,
|
||||
trailing: appIcon(request),
|
||||
onDelete: deleteHost,
|
||||
onExportHar: exportDomainHar,
|
||||
onRequestRemove: (req) {
|
||||
widget.onRemove?.call([req]);
|
||||
changeState();
|
||||
@@ -281,8 +287,41 @@ class DomainWidgetState extends State<DomainList> with AutomaticKeepAliveClientM
|
||||
return container.expand((list) => list.body.map((it) => it.request)).toList();
|
||||
}
|
||||
|
||||
Future<void> exportDomainHar(String domain) async {
|
||||
var requests = containerMap[domain]?.body.map((it) => it.request).toList() ?? [];
|
||||
if (requests.isEmpty) {
|
||||
if (mounted) FlutterToastr.show(localizations.emptyData, context);
|
||||
return;
|
||||
}
|
||||
|
||||
var fileName = _domainHarFileName(domain);
|
||||
var path = await FilePicker.platform.saveFile(fileName: fileName);
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var file = await File(path).create(recursive: true);
|
||||
await Har.writeFile(requests, file, title: fileName);
|
||||
if (mounted) FlutterToastr.show(localizations.exportSuccess, context);
|
||||
} catch (e) {
|
||||
if (mounted) FlutterToastr.show('${localizations.exportFailed} $e', context);
|
||||
}
|
||||
}
|
||||
|
||||
String _domainHarFileName(String domain) {
|
||||
var uri = Uri.tryParse(domain);
|
||||
var host = (uri?.host.isNotEmpty == true) ? uri!.host : domain;
|
||||
var suffix = uri?.hasPort == true ? '_${uri!.port}' : '';
|
||||
var safeDomain = '$host$suffix'.replaceAll(RegExp(r'[^A-Za-z0-9._-]'), '_');
|
||||
if (safeDomain.isEmpty) {
|
||||
safeDomain = 'domain';
|
||||
}
|
||||
return 'ProxyPin_${safeDomain}_${DateTime.now().dateFormat()}.har';
|
||||
}
|
||||
|
||||
///排序
|
||||
sort(bool desc) {
|
||||
void sort(bool desc) {
|
||||
sortDesc = desc;
|
||||
containerMap.forEach((key, request) {
|
||||
var reversed = request.body.toList().reversed;
|
||||
@@ -310,10 +349,16 @@ class DomainRequests extends StatefulWidget {
|
||||
|
||||
//移除回调
|
||||
final Function(String host)? onDelete;
|
||||
final Function(String host)? onExportHar;
|
||||
final Function(HttpRequest request)? onRequestRemove;
|
||||
|
||||
DomainRequests(this.domain,
|
||||
{this.selected = false, this.onDelete, required this.proxyServer, this.onRequestRemove, this.trailing})
|
||||
{this.selected = false,
|
||||
this.onDelete,
|
||||
this.onExportHar,
|
||||
required this.proxyServer,
|
||||
this.onRequestRemove,
|
||||
this.trailing})
|
||||
: super(key: GlobalKey<_DomainRequestsState>());
|
||||
|
||||
///添加请求
|
||||
@@ -368,6 +413,7 @@ class DomainRequests extends StatefulWidget {
|
||||
trailing: trailing,
|
||||
selected: selected ?? state.currentState?.selected == true,
|
||||
onDelete: onDelete,
|
||||
onExportHar: onExportHar,
|
||||
onRequestRemove: onRequestRemove,
|
||||
proxyServer: proxyServer);
|
||||
if (body != null) {
|
||||
@@ -479,6 +525,8 @@ class _DomainRequestsState extends State<DomainRequests> {
|
||||
submenu: hostFilterMenu(),
|
||||
),
|
||||
MenuItem.separator(),
|
||||
MenuItem(label: localizations.exportDomainHar, onClick: (_) => exportDomainHar()),
|
||||
MenuItem.separator(),
|
||||
MenuItem(label: localizations.repeatDomainRequests, onClick: (_) => repeatDomainRequests()),
|
||||
MenuItem.separator(),
|
||||
MenuItem(label: localizations.delete, onClick: (_) => _delete()),
|
||||
@@ -502,6 +550,10 @@ class _DomainRequestsState extends State<DomainRequests> {
|
||||
}
|
||||
}
|
||||
|
||||
void exportDomainHar() {
|
||||
widget.onExportHar?.call(widget.domain);
|
||||
}
|
||||
|
||||
Menu hostFilterMenu() {
|
||||
return Menu(items: [
|
||||
MenuItem(
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
*/
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:date_format/date_format.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:proxypin/l10n/app_localizations.dart';
|
||||
@@ -29,6 +31,8 @@ import 'package:proxypin/network/http/http.dart';
|
||||
import 'package:proxypin/network/http/http_client.dart';
|
||||
import 'package:proxypin/ui/component/widgets.dart';
|
||||
import 'package:proxypin/ui/mobile/request/request_sequence.dart';
|
||||
import 'package:proxypin/utils/har.dart';
|
||||
import 'package:proxypin/utils/lang.dart';
|
||||
import 'package:proxypin/utils/listenable_list.dart';
|
||||
|
||||
///域名列表
|
||||
@@ -297,6 +301,12 @@ class DomainListState extends State<DomainList> with AutomaticKeepAliveClientMix
|
||||
repeatDomainRequests(hostAndPort);
|
||||
}),
|
||||
const Divider(thickness: 0.5, height: 5),
|
||||
BottomSheetItem(
|
||||
text: localizations.exportDomainHar,
|
||||
onPressed: () {
|
||||
exportDomainHar(hostAndPort);
|
||||
}),
|
||||
const Divider(thickness: 0.5, height: 5),
|
||||
BottomSheetItem(
|
||||
text: localizations.delete,
|
||||
onPressed: () {
|
||||
@@ -345,4 +355,32 @@ class DomainListState extends State<DomainList> with AutomaticKeepAliveClientMix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> exportDomainHar(HostAndPort hostAndPort) async {
|
||||
var requests = containerMap[hostAndPort] ?? [];
|
||||
if (requests.isEmpty) {
|
||||
if (mounted) FlutterToastr.show(localizations.emptyData, context);
|
||||
return;
|
||||
}
|
||||
|
||||
var fileName = _domainHarFileName(hostAndPort);
|
||||
var json = await Har.writeJson(requests, title: fileName);
|
||||
var bytes = utf8.encode(json);
|
||||
|
||||
var path = await FilePicker.platform.saveFile(fileName: fileName, bytes: bytes);
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mounted) FlutterToastr.show(localizations.exportSuccess, context);
|
||||
}
|
||||
|
||||
String _domainHarFileName(HostAndPort hostAndPort) {
|
||||
var suffix = (hostAndPort.port == 80 || hostAndPort.port == 443) ? '' : '_${hostAndPort.port}';
|
||||
var safeDomain = '${hostAndPort.host}$suffix'.replaceAll(RegExp(r'[^A-Za-z0-9._-]'), '_');
|
||||
if (safeDomain.isEmpty) {
|
||||
safeDomain = 'domain';
|
||||
}
|
||||
return 'ProxyPin_${safeDomain}_${DateTime.now().dateFormat()}.har';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user