手机版历史记录分享&导入

This commit is contained in:
wanghongen
2023-09-23 02:11:40 +08:00
parent fbf6e33f56
commit 5ae1e0212b
16 changed files with 282 additions and 165 deletions

View File

@@ -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,

View File

@@ -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();

View File

@@ -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(

View File

@@ -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 = "";

View File

@@ -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,

View File

@@ -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),

View File

@@ -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);
}
})

View File

@@ -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,
)))
])
];
}