mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-05-22 16:25:49 +08:00
手机版历史记录分享&导入
This commit is contained in:
@@ -49,11 +49,7 @@ class DrawerWidget extends StatelessWidget {
|
||||
leading: const Icon(Icons.history),
|
||||
title: const Text("历史"),
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
onTap: () => navigator(
|
||||
context,
|
||||
Scaffold(
|
||||
appBar: AppBar(title: const Text("历史记录", style: TextStyle(fontSize: 16)), centerTitle: true),
|
||||
body: MobileHistory(proxyServer: proxyServer, requestStateKey: requestStateKey))),
|
||||
onTap: () => navigator(context, MobileHistory(proxyServer: proxyServer, requestStateKey: requestStateKey)),
|
||||
),
|
||||
const Divider(thickness: 0.3),
|
||||
Padding(padding: const EdgeInsets.only(left: 15), child: PortWidget(proxyServer: proxyServer)),
|
||||
@@ -139,25 +135,25 @@ class MoreEnum extends StatelessWidget {
|
||||
})),
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.qr_code_scanner_outlined),
|
||||
title: const Text("连接终端"),
|
||||
onTap: () {
|
||||
connectRemote(context);
|
||||
},
|
||||
)),
|
||||
dense: true,
|
||||
leading: const Icon(Icons.qr_code_scanner_outlined),
|
||||
title: const Text("连接终端"),
|
||||
onTap: () {
|
||||
connectRemote(context);
|
||||
},
|
||||
)),
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.phone_iphone),
|
||||
title: const Text("我的二维码"),
|
||||
onTap: () async {
|
||||
var ip = await localIp();
|
||||
if (context.mounted) {
|
||||
connectQrCode(context, ip, proxyServer.port);
|
||||
}
|
||||
},
|
||||
)),
|
||||
dense: true,
|
||||
leading: const Icon(Icons.phone_iphone),
|
||||
title: const Text("我的二维码"),
|
||||
onTap: () async {
|
||||
var ip = await localIp();
|
||||
if (context.mounted) {
|
||||
connectQrCode(context, ip, proxyServer.port);
|
||||
}
|
||||
},
|
||||
)),
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
|
||||
@@ -114,7 +114,8 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener {
|
||||
'2. 请求重写增加名称&URL参数重写;\n'
|
||||
'3. 请求重写增加重定向;\n'
|
||||
'4. 建立连接异常显示请求体;\n'
|
||||
'5. 请求编辑重发响应体查看增加多种格式,详情Body体增加快速解码入口;';
|
||||
'5. 请求编辑重发响应体查看增加多种格式,详情Body体增加快速解码入口\n'
|
||||
'6. 请求列表增加编号;';
|
||||
showAlertDialog('更新内容V1.0.3', content, () {
|
||||
widget.configuration.upgradeNoticeV3 = false;
|
||||
widget.configuration.flushConfig();
|
||||
|
||||
@@ -45,6 +45,7 @@ class _FavoritesState extends State<MobileFavorites> {
|
||||
var request = favorites.elementAt(index);
|
||||
return _FavoriteItem(
|
||||
request,
|
||||
index: index,
|
||||
onRemove: (HttpRequest request) {
|
||||
FavoriteStorage.removeFavorite(request);
|
||||
FlutterToastr.show('已删除收藏', context);
|
||||
@@ -63,11 +64,13 @@ class _FavoritesState extends State<MobileFavorites> {
|
||||
}
|
||||
|
||||
class _FavoriteItem extends StatefulWidget {
|
||||
final int index;
|
||||
final ProxyServer proxyServer;
|
||||
final HttpRequest request;
|
||||
final Function(HttpRequest request)? onRemove;
|
||||
|
||||
const _FavoriteItem(this.request, {Key? key, required this.onRemove, required this.proxyServer}) : super(key: key);
|
||||
const _FavoriteItem(this.request, {Key? key, required this.onRemove, required this.proxyServer, required this.index})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<_FavoriteItem> createState() => _FavoriteItemState();
|
||||
@@ -80,14 +83,19 @@ class _FavoriteItemState extends State<_FavoriteItem> {
|
||||
var response = request.response;
|
||||
var title = '${request.method.name} ${request.requestUrl}';
|
||||
var time = formatDate(request.requestTime, [mm, '-', d, ' ', HH, ':', nn, ':', ss]);
|
||||
String subtitle =
|
||||
'$time - [${response?.status.code ?? ''}] ${response?.contentType.name.toUpperCase() ?? ''} ${response?.costTime() ?? ''} ';
|
||||
return ListTile(
|
||||
onLongPress: menu,
|
||||
minLeadingWidth: 25,
|
||||
leading: getIcon(response),
|
||||
title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 2),
|
||||
subtitle: Text(
|
||||
'$time - [${response?.status.code ?? ''}] ${response?.contentType.name.toUpperCase() ?? ''} ${response?.costTime() ?? ''} ',
|
||||
maxLines: 1),
|
||||
subtitle: Text.rich(
|
||||
maxLines: 1,
|
||||
TextSpan(children: [
|
||||
TextSpan(text: '#${widget.index} ', style: const TextStyle(fontSize: 12, color: Colors.teal)),
|
||||
TextSpan(text: subtitle, style: const TextStyle(fontSize: 12)),
|
||||
])),
|
||||
dense: true,
|
||||
onTap: onClick);
|
||||
}
|
||||
@@ -130,6 +138,7 @@ class _FavoriteItemState extends State<_FavoriteItem> {
|
||||
child: const SizedBox(width: double.infinity, child: Text("删除收藏", textAlign: TextAlign.center)),
|
||||
onPressed: () {
|
||||
widget.onRemove?.call(widget.request);
|
||||
FlutterToastr.show('删除成功', context);
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
Container(
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:date_format/date_format.dart';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_toastr/flutter_toastr.dart';
|
||||
@@ -14,6 +15,7 @@ import 'package:network_proxy/network/http/http.dart';
|
||||
import 'package:network_proxy/storage/histories.dart';
|
||||
import 'package:network_proxy/ui/component/utils.dart';
|
||||
import 'package:network_proxy/ui/mobile/request/list.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../../../utils/har.dart';
|
||||
|
||||
@@ -51,14 +53,19 @@ class _MobileHistoryState extends State<MobileHistory> {
|
||||
children.add(buildItem(data, i, entry));
|
||||
}
|
||||
|
||||
if (children.isEmpty) {
|
||||
return const Center(child: Text("暂无历史记录"));
|
||||
}
|
||||
return ListView.separated(
|
||||
itemCount: children.length,
|
||||
itemBuilder: (context, index) => children[index],
|
||||
separatorBuilder: (_, index) => const Divider(thickness: 0.3, height: 0),
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("历史记录", style: TextStyle(fontSize: 16)),
|
||||
centerTitle: true,
|
||||
actions: [TextButton(onPressed: () => import(data), child: const Text("导入"))],
|
||||
),
|
||||
body: children.isEmpty
|
||||
? const Center(child: Text("暂无历史记录"))
|
||||
: ListView.separated(
|
||||
itemCount: children.length,
|
||||
itemBuilder: (context, index) => children[index],
|
||||
separatorBuilder: (_, index) => const Divider(thickness: 0.3, height: 0),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -83,9 +90,35 @@ class _MobileHistoryState extends State<MobileHistory> {
|
||||
onTap: () {});
|
||||
}
|
||||
|
||||
//导入har
|
||||
import(HistoryStorage storage) async {
|
||||
const XTypeGroup typeGroup = XTypeGroup(
|
||||
label: 'Har',
|
||||
);
|
||||
final XFile? file = await openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
print(file);
|
||||
try {
|
||||
var historyItem = await storage.addHarFile(file);
|
||||
setState(() {
|
||||
Navigator.pushNamed(context, '/domain', arguments: {'item': historyItem});
|
||||
FlutterToastr.show("导入成功", context);
|
||||
});
|
||||
} catch (e, t) {
|
||||
print(e);
|
||||
print(t);
|
||||
if (context.mounted) {
|
||||
FlutterToastr.show("导入失败 $e", context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//写入文件
|
||||
_writeHarFile(HistoryStorage storage, List<HttpRequest> container, String name) async {
|
||||
var file = await HistoryStorage.openFile("${DateTime.now().millisecondsSinceEpoch}.txt");
|
||||
var file = await HistoryStorage.openFile("${DateTime.now().millisecondsSinceEpoch.toRadixString(36)}.txt");
|
||||
print(file);
|
||||
RandomAccessFile open = await file.open(mode: FileMode.append);
|
||||
HistoryItem history = await storage.addHistory(name, file, 0);
|
||||
@@ -107,6 +140,7 @@ class _MobileHistoryState extends State<MobileHistory> {
|
||||
HapticFeedback.heavyImpact();
|
||||
showContextMenu(context, detail.globalPosition.translate(-50, index == 0 ? -100 : 100), items: [
|
||||
PopupMenuItem(child: const Text("重命名"), onTap: () => renameHistory(storage, item)),
|
||||
PopupMenuItem(child: const Text("分享"), onTap: () => export(storage, item)),
|
||||
const PopupMenuDivider(height: 0.3),
|
||||
PopupMenuItem(child: const Text("删除"), onTap: () => deleteHistory(storage, index))
|
||||
]);
|
||||
@@ -130,6 +164,19 @@ class _MobileHistoryState extends State<MobileHistory> {
|
||||
));
|
||||
}
|
||||
|
||||
//导出har
|
||||
export(HistoryStorage storage, HistoryItem item) async {
|
||||
//文件名称
|
||||
String fileName =
|
||||
'${item.name.contains("ProxyPin") ? '' : 'ProxyPin'}${item.name}.har'.replaceAll(" ", "_").replaceAll(":", "_");
|
||||
//获取请求
|
||||
List<HttpRequest> requests = await storage.getRequests(item);
|
||||
var json = await Har.writeJson(requests, title: item.name);
|
||||
var file = XFile.fromData(Uint8List.fromList(json.codeUnits), name: fileName, mimeType: "har");
|
||||
Share.shareXFiles([file], subject: fileName);
|
||||
Future.delayed(const Duration(seconds: 30), () => item.requests = null);
|
||||
}
|
||||
|
||||
//重命名
|
||||
renameHistory(HistoryStorage storage, HistoryItem item) {
|
||||
String name = "";
|
||||
|
||||
@@ -215,6 +215,7 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
|
||||
GlobalKey<RequestRowState> key = GlobalKey();
|
||||
indexes[view.elementAt(index)] = key;
|
||||
return RequestRow(
|
||||
index: index,
|
||||
key: key,
|
||||
request: view.elementAt(index),
|
||||
proxyServer: widget.proxyServer,
|
||||
|
||||
@@ -14,13 +14,14 @@ import 'package:network_proxy/utils/curl.dart';
|
||||
|
||||
///请求行
|
||||
class RequestRow extends StatefulWidget {
|
||||
final int index;
|
||||
final HttpRequest request;
|
||||
final ProxyServer proxyServer;
|
||||
final bool displayDomain;
|
||||
final Function(HttpRequest)? onRemove;
|
||||
|
||||
const RequestRow(
|
||||
{super.key, required this.request, required this.proxyServer, this.displayDomain = true, this.onRemove});
|
||||
{super.key, required this.request, required this.proxyServer, this.displayDomain = true, this.onRemove, required this.index});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
@@ -51,12 +52,16 @@ class RequestRowState extends State<RequestRow> {
|
||||
var time = formatDate(request.requestTime, [HH, ':', nn, ':', ss]);
|
||||
var subTitle =
|
||||
'$time - [${response?.status.code ?? ''}] ${response?.contentType.name.toUpperCase() ?? ''} ${response?.costTime() ?? ''}';
|
||||
|
||||
return ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
leading: getIcon(response),
|
||||
title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 2, style: const TextStyle(fontSize: 14)),
|
||||
subtitle: Text(subTitle, maxLines: 1, style: const TextStyle(fontSize: 12)),
|
||||
subtitle: Text.rich(
|
||||
maxLines: 1,
|
||||
TextSpan(children: [
|
||||
TextSpan(text: '#${widget.index} ', style: const TextStyle(fontSize: 12, color: Colors.teal)),
|
||||
TextSpan(text: subTitle, style: const TextStyle(fontSize: 12)),
|
||||
])),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
|
||||
@@ -76,16 +76,32 @@ class _MobileRequestRewriteState extends State<MobileRequestRewrite> {
|
||||
icon: const Icon(Icons.remove, size: 18),
|
||||
label: const Text("删除", style: TextStyle(fontSize: 14)),
|
||||
onPressed: () {
|
||||
var removeSelected = requestRuleList.removeSelected();
|
||||
if (removeSelected.isEmpty) {
|
||||
var selected = requestRuleList.currentSelectedIndex();
|
||||
if (selected < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
changed = true;
|
||||
setState(() {
|
||||
widget.configuration.requestRewrites.removeIndex(removeSelected);
|
||||
requestRuleList.changeState();
|
||||
});
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
return AlertDialog(
|
||||
title: const Text("是否删除该请求重写?", style: TextStyle(fontSize: 18)),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text("取消")),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
changed = true;
|
||||
setState(() {
|
||||
widget.configuration.requestRewrites.removeIndex(requestRuleList.removeSelected());
|
||||
requestRuleList.changeState();
|
||||
});
|
||||
FlutterToastr.show('删除成功', context);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text("删除")),
|
||||
],
|
||||
);
|
||||
});
|
||||
})
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
@@ -161,7 +177,7 @@ class _RewriteRuleState extends State<RewriteRule> {
|
||||
RequestRewrites.instance.addRule(rule);
|
||||
}
|
||||
|
||||
FlutterToastr.show("添加请求重写规则成功", context);
|
||||
FlutterToastr.show("保存请求重写规则成功", context);
|
||||
Navigator.of(context).pop(rule);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -52,16 +52,13 @@ class _MobileSslState extends State<MobileSslWidget> {
|
||||
expandedAlignment: Alignment.topLeft,
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||
shape: const Border(),
|
||||
children: [
|
||||
TextButton(onPressed: () => _downloadCert(), child: const Text("1. 点击下载根证书")),
|
||||
...(Platform.isIOS ? ios() : android()),
|
||||
const SizedBox(height: 20)
|
||||
])
|
||||
children: [...(Platform.isIOS ? ios() : android()), const SizedBox(height: 20)])
|
||||
]));
|
||||
}
|
||||
|
||||
List<Widget> ios() {
|
||||
return [
|
||||
TextButton(onPressed: () => _downloadCert(), child: const Text("1. 点击下载根证书")),
|
||||
TextButton(onPressed: () {}, child: const Text("2. 安装根证书 -> 信任证书")),
|
||||
TextButton(onPressed: () {}, child: const Text("2.1 安装根证书 设置 > 已下载描述文件 > 安装")),
|
||||
Padding(
|
||||
@@ -78,16 +75,40 @@ class _MobileSslState extends State<MobileSslWidget> {
|
||||
|
||||
List<Widget> android() {
|
||||
return [
|
||||
TextButton(onPressed: () => _downloadCert(), child: const Text("1.1系统根证书将根证书命名成 243f0bfb.0")),
|
||||
TextButton(onPressed: () {}, child: const Text("2. 打开设置 -> 安全 -> 加密和凭据 -> 安装证书 -> CA 证书")),
|
||||
ClipRRect(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
heightFactor: .7,
|
||||
child: Image.network(
|
||||
"https://foruda.gitee.com/images/1689352695624941051/74e3bed6_1073801.png",
|
||||
height: 680,
|
||||
)))
|
||||
ExpansionTile(
|
||||
title: const Text("Root用户:", style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14)),
|
||||
tilePadding: const EdgeInsets.only(left: 0),
|
||||
expandedAlignment: Alignment.topLeft,
|
||||
initiallyExpanded: true,
|
||||
shape: const Border(),
|
||||
children: [
|
||||
const Text("针对安卓Root用户做了个Magisk模块ProxyPinCA系统证书,安装完重启手机即可。"),
|
||||
TextButton(
|
||||
child: const Text("https://gitee.com/wanghongenpin/Magisk-ProxyPinCA/releases/tag/1.0.0"),
|
||||
onPressed: () {
|
||||
launchUrl(Uri.parse("https://gitee.com/wanghongenpin/Magisk-ProxyPinCA/releases/tag/1.0.0"));
|
||||
})
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
ExpansionTile(
|
||||
title: const Text("非Root用户:", style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14)),
|
||||
tilePadding: const EdgeInsets.only(left: 0),
|
||||
expandedAlignment: Alignment.topLeft,
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||
initiallyExpanded: true,
|
||||
shape: const Border(),
|
||||
children: [
|
||||
TextButton(onPressed: () => _downloadCert(), child: const Text("1. 点击下载根证书")),
|
||||
TextButton(onPressed: () {}, child: const Text("2. 打开设置 -> 安全 -> 加密和凭据 -> 安装证书 -> CA 证书")),
|
||||
ClipRRect(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
heightFactor: .7,
|
||||
child: Image.network(
|
||||
"https://foruda.gitee.com/images/1689352695624941051/74e3bed6_1073801.png",
|
||||
height: 680,
|
||||
)))
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user