mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-05-20 16:15:47 +08:00
手机端高级搜索
This commit is contained in:
@@ -138,27 +138,33 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
|
||||
panel = NetworkTabController(tabStyle: const TextStyle(fontSize: 18), proxyServer: proxyServer);
|
||||
|
||||
if (widget.configuration.guide) {
|
||||
//首次引导
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) {
|
||||
return AlertDialog(
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
widget.configuration.guide = false;
|
||||
widget.configuration.flushConfig();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('关闭'))
|
||||
],
|
||||
title: const Text('提示', style: TextStyle(fontSize: 18)),
|
||||
content: const Text('默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n'
|
||||
'点击的HTTPS抓包(加锁图标),选择安装根证书,按照提示操作即可。'));
|
||||
});
|
||||
|
||||
return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
//首次引导
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) {
|
||||
return AlertDialog(
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
widget.configuration.guide = false;
|
||||
widget.configuration.flushConfig();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('关闭'))
|
||||
],
|
||||
title: const Text('提示', style: TextStyle(fontSize: 18)),
|
||||
content: const Text(
|
||||
'默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n'
|
||||
'点击的HTTPS抓包(加锁图标),选择安装根证书,按照提示操作即可。\n\n'
|
||||
'新增更新:\n'
|
||||
'1. 增加高级搜索,点击搜索Icon触发。\n'
|
||||
'2. 显示SSL握手异常、建立连接异常、未知异常等请求。\n'
|
||||
'3.响应体大时异步加载json,请求重写增加域名,修复手机扫码连接未开启代理时不转发问题',
|
||||
style: TextStyle(fontSize: 14)));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import 'dart:typed_data';
|
||||
import 'package:network_proxy/network/http/body_reader.dart';
|
||||
|
||||
import '../../utils/compress.dart';
|
||||
import '../util/logger.dart';
|
||||
import 'http.dart';
|
||||
import 'http_headers.dart';
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'package:network_proxy/network/http/http.dart';
|
||||
import 'package:network_proxy/network/util/attribute_keys.dart';
|
||||
import 'package:network_proxy/network/util/host_filter.dart';
|
||||
import 'package:network_proxy/ui/component/transition.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/model/search.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/model/search_model.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/path.dart';
|
||||
import 'package:network_proxy/ui/content/panel.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/search.dart';
|
||||
@@ -92,7 +92,7 @@ class DomainWidgetState extends State<DomainWidget> {
|
||||
headerBody.addBody(channel.id, listURI);
|
||||
|
||||
//搜索视图
|
||||
if (searchModel?.isNotEmpty == true && headerBody.filter(listURI, searchModel!)) {
|
||||
if (searchModel?.isNotEmpty == true && searchModel?.filter(request, null) == true) {
|
||||
searchView[hostAndPort]?.addBody(channel.id, listURI);
|
||||
}
|
||||
return;
|
||||
@@ -122,7 +122,7 @@ class DomainWidgetState extends State<DomainWidget> {
|
||||
}
|
||||
|
||||
//搜索视图
|
||||
if (searchModel?.isNotEmpty == true && headerBody?.filter(pathRow, searchModel!) == true) {
|
||||
if (searchModel?.isNotEmpty == true && searchModel?.filter(pathRow.request, response) == true) {
|
||||
var header = searchView[hostAndPort];
|
||||
if (header?.getBody(channel.id) == null) {
|
||||
header?.addBody(channel.id, pathRow);
|
||||
@@ -175,7 +175,7 @@ class HeaderBody extends StatefulWidget {
|
||||
|
||||
///根据文本过滤
|
||||
Iterable<PathRow> search(SearchModel searchModel) {
|
||||
return _body.where((element) => filter(element, searchModel));
|
||||
return _body.where((element) => searchModel.filter(element.request, element.response.get()));
|
||||
}
|
||||
|
||||
///复制
|
||||
@@ -203,68 +203,6 @@ class HeaderBody extends StatefulWidget {
|
||||
State<StatefulWidget> createState() {
|
||||
return _HeaderBodyState();
|
||||
}
|
||||
|
||||
bool filter(PathRow element, SearchModel searchModel) {
|
||||
var request = element.request;
|
||||
var response = element.response.get();
|
||||
|
||||
if (searchModel.requestMethod != null && searchModel.requestMethod != request.method) {
|
||||
return false;
|
||||
}
|
||||
if (searchModel.requestContentType != null && request.contentType != searchModel.requestContentType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (searchModel.responseContentType != null && response?.contentType != searchModel.responseContentType) {
|
||||
return false;
|
||||
}
|
||||
if (searchModel.statusCode != null && response?.status.code != searchModel.statusCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (searchModel.keyword == null || searchModel.keyword?.isEmpty == true || searchModel.searchOptions.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (var option in searchModel.searchOptions) {
|
||||
if (keywordFilter(searchModel.keyword!, option, request, response)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool keywordFilter(String keyword, Option option, HttpRequest request, HttpResponse? response) {
|
||||
if (option == Option.url && request.uri.toString().toLowerCase().contains(keyword.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (option == Option.requestBody && request.bodyAsString.contains(keyword) == true) {
|
||||
return true;
|
||||
}
|
||||
if (option == Option.responseBody && response?.bodyAsString.contains(keyword) == true) {
|
||||
return true;
|
||||
}
|
||||
if (option == Option.method && request.method.name.toLowerCase() == keyword.toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
if (option == Option.responseContentType && response?.headers.contentType.contains(keyword) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (option == Option.requestHeader || option == Option.responseHeader) {
|
||||
print(response?.headers.entries);
|
||||
var entries = option == Option.requestHeader ? request.headers.entries : response?.headers.entries ?? [];
|
||||
|
||||
for (var entry in entries) {
|
||||
if (entry.value.any((element) => element.contains(keyword))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class _HeaderBodyState extends State<HeaderBody> {
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import 'package:network_proxy/network/http/http.dart';
|
||||
|
||||
/// @author wanghongen
|
||||
/// 2023/8/4
|
||||
class SearchModel {
|
||||
String? keyword;
|
||||
|
||||
//搜索范围
|
||||
Set<Option> searchOptions = {Option.url};
|
||||
|
||||
//请求方法
|
||||
HttpMethod? requestMethod;
|
||||
ContentType? requestContentType;
|
||||
ContentType? responseContentType;
|
||||
|
||||
//状态码
|
||||
int? statusCode;
|
||||
|
||||
SearchModel([this.keyword]);
|
||||
|
||||
bool get isNotEmpty {
|
||||
return keyword?.trim().isNotEmpty == true || requestMethod != null ||
|
||||
requestContentType != null || responseContentType != null || statusCode != null;
|
||||
}
|
||||
|
||||
of(SearchModel searchModel) {
|
||||
keyword = searchModel.keyword;
|
||||
searchOptions = searchModel.searchOptions;
|
||||
requestContentType = searchModel.requestContentType;
|
||||
requestMethod = searchModel.requestMethod;
|
||||
requestContentType = searchModel.requestContentType;
|
||||
statusCode = searchModel.statusCode;
|
||||
}
|
||||
|
||||
///清空对象
|
||||
clear() {
|
||||
keyword = null;
|
||||
requestContentType = null;
|
||||
searchOptions.clear();
|
||||
requestMethod = null;
|
||||
requestContentType = null;
|
||||
statusCode = null;
|
||||
}
|
||||
|
||||
///复制对象
|
||||
SearchModel clone() {
|
||||
var searchModel = SearchModel(keyword);
|
||||
searchModel.searchOptions = searchOptions;
|
||||
searchModel.requestMethod = requestMethod;
|
||||
searchModel.requestContentType = requestContentType;
|
||||
searchModel.responseContentType = responseContentType;
|
||||
searchModel.statusCode = statusCode;
|
||||
return searchModel;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SearchModel{keyword: $keyword, searchOptions: $searchOptions, responseContentType: $responseContentType, requestMethod: $requestMethod, requestContentType: $requestContentType, statusCode: $statusCode}';
|
||||
}
|
||||
}
|
||||
|
||||
enum Option {
|
||||
url,
|
||||
method,
|
||||
responseContentType,
|
||||
requestHeader,
|
||||
requestBody,
|
||||
responseHeader,
|
||||
responseBody,
|
||||
}
|
||||
126
lib/ui/desktop/left/model/search_model.dart
Normal file
126
lib/ui/desktop/left/model/search_model.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'package:network_proxy/network/http/http.dart';
|
||||
|
||||
/// @author wanghongen
|
||||
/// 2023/8/4
|
||||
class SearchModel {
|
||||
String? keyword;
|
||||
|
||||
//搜索范围
|
||||
Set<Option> searchOptions = {Option.url};
|
||||
|
||||
//请求方法
|
||||
HttpMethod? requestMethod;
|
||||
|
||||
//请求类型
|
||||
ContentType? requestContentType;
|
||||
|
||||
//响应类型
|
||||
ContentType? responseContentType;
|
||||
|
||||
//状态码
|
||||
int? statusCode;
|
||||
|
||||
SearchModel([this.keyword]);
|
||||
|
||||
bool get isNotEmpty {
|
||||
return keyword?.trim().isNotEmpty == true ||
|
||||
requestMethod != null ||
|
||||
requestContentType != null ||
|
||||
responseContentType != null ||
|
||||
statusCode != null;
|
||||
}
|
||||
|
||||
bool get isEmpty {
|
||||
return !isNotEmpty;
|
||||
}
|
||||
|
||||
///是否匹配
|
||||
bool filter(HttpRequest request, HttpResponse? response) {
|
||||
if (isEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (requestMethod != null && requestMethod != request.method) {
|
||||
return false;
|
||||
}
|
||||
if (requestContentType != null && request.contentType != requestContentType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (responseContentType != null && response?.contentType != responseContentType) {
|
||||
return false;
|
||||
}
|
||||
if (statusCode != null && response?.status.code != statusCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (keyword == null || keyword?.isEmpty == true || searchOptions.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (var option in searchOptions) {
|
||||
if (keywordFilter(keyword!, option, request, response)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
///关键字过滤
|
||||
bool keywordFilter(String keyword, Option option, HttpRequest request, HttpResponse? response) {
|
||||
if (option == Option.url && request.requestUrl.toLowerCase().contains(keyword.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (option == Option.requestBody && request.bodyAsString.contains(keyword) == true) {
|
||||
return true;
|
||||
}
|
||||
if (option == Option.responseBody && response?.bodyAsString.contains(keyword) == true) {
|
||||
return true;
|
||||
}
|
||||
if (option == Option.method && request.method.name.toLowerCase() == keyword.toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
if (option == Option.responseContentType && response?.headers.contentType.contains(keyword) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (option == Option.requestHeader || option == Option.responseHeader) {
|
||||
var entries = option == Option.requestHeader ? request.headers.entries : response?.headers.entries ?? [];
|
||||
|
||||
for (var entry in entries) {
|
||||
if (entry.value.any((element) => element.contains(keyword))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
///复制对象
|
||||
SearchModel clone() {
|
||||
var searchModel = SearchModel(keyword);
|
||||
searchModel.searchOptions = searchOptions;
|
||||
searchModel.requestMethod = requestMethod;
|
||||
searchModel.requestContentType = requestContentType;
|
||||
searchModel.responseContentType = responseContentType;
|
||||
searchModel.statusCode = statusCode;
|
||||
return searchModel;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SearchModel{keyword: $keyword, searchOptions: $searchOptions, responseContentType: $responseContentType, requestMethod: $requestMethod, requestContentType: $requestContentType, statusCode: $statusCode}';
|
||||
}
|
||||
}
|
||||
|
||||
enum Option {
|
||||
url,
|
||||
method,
|
||||
responseContentType,
|
||||
requestHeader,
|
||||
requestBody,
|
||||
responseHeader,
|
||||
responseBody,
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:network_proxy/network/http/http.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/model/search.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/model/search_model.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/search_condition.dart';
|
||||
|
||||
class Search extends StatefulWidget {
|
||||
@@ -33,9 +33,6 @@ class _SearchState extends State<Search> {
|
||||
cursorHeight: 22,
|
||||
controller: keywordController,
|
||||
onChanged: (val) async {
|
||||
if (searchModel.keyword == val) {
|
||||
return;
|
||||
}
|
||||
searchModel.keyword = val;
|
||||
|
||||
if (!changing) {
|
||||
@@ -71,34 +68,27 @@ class _SearchState extends State<Search> {
|
||||
if (!searched) {
|
||||
searchModel.searchOptions = {Option.url};
|
||||
}
|
||||
showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy - 380,
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy - 380,
|
||||
),
|
||||
items: [
|
||||
PopupMenuItem(
|
||||
padding: const EdgeInsets.only(left: 15, right: 15, top: 10),
|
||||
enabled: false,
|
||||
child: DefaultTextStyle.merge(
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
child: SizedBox(
|
||||
width: 500,
|
||||
height: 350,
|
||||
child: SearchConditions(
|
||||
searchModel: searchModel,
|
||||
onSearch: (val) {
|
||||
setState(() {
|
||||
searchModel = val;
|
||||
searched = searchModel.isNotEmpty;
|
||||
keywordController.text = searchModel.keyword ?? '';
|
||||
widget.onSearch?.call(searchModel);
|
||||
});
|
||||
}))))
|
||||
]);
|
||||
var height = MediaQuery.of(context).size.height;
|
||||
showMenu(context: context, position: RelativeRect.fromLTRB(10, height - 410, 10, height - 410), items: [
|
||||
PopupMenuItem(
|
||||
padding: const EdgeInsets.only(left: 15, right: 15, top: 10),
|
||||
enabled: false,
|
||||
child: DefaultTextStyle.merge(
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 14),
|
||||
child: SizedBox(
|
||||
width: 500,
|
||||
height: 350,
|
||||
child: SearchConditions(
|
||||
searchModel: searchModel,
|
||||
onSearch: (val) {
|
||||
setState(() {
|
||||
searchModel = val;
|
||||
searched = searchModel.isNotEmpty;
|
||||
keywordController.text = searchModel.keyword ?? '';
|
||||
widget.onSearch?.call(searchModel);
|
||||
});
|
||||
}))))
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:network_proxy/network/http/http.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/model/search.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/model/search_model.dart';
|
||||
import 'package:network_proxy/utils/lang.dart';
|
||||
|
||||
/// @author wanghongen
|
||||
@@ -10,8 +10,9 @@ import 'package:network_proxy/utils/lang.dart';
|
||||
class SearchConditions extends StatefulWidget {
|
||||
final SearchModel searchModel;
|
||||
final Function(SearchModel searchModel)? onSearch;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
const SearchConditions({super.key, required this.searchModel, this.onSearch});
|
||||
const SearchConditions({super.key, required this.searchModel, this.onSearch, this.padding});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
@@ -46,101 +47,103 @@ class SearchConditionsState extends State<SearchConditions> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
initialValue: searchModel.keyword,
|
||||
onChanged: (val) => searchModel.keyword = val,
|
||||
decoration: const InputDecoration(
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.all(10),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(15))),
|
||||
hintText: '关键词',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
const Text("关键词搜索范围:"),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
return Container(
|
||||
padding: widget.padding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
options('URL', Option.url),
|
||||
options('请求头', Option.requestHeader),
|
||||
options('请求体', Option.requestBody),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [options('响应头', Option.responseHeader), options('响应体', Option.responseBody)],
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
row(
|
||||
const Text('请求方法:'),
|
||||
DropdownMenu(
|
||||
initialValue: searchModel.requestMethod?.name ?? '全部',
|
||||
items: HttpMethod.values.map((e) => e.name).toList()..insert(0, '全部'),
|
||||
onSelected: (String value) {
|
||||
searchModel.requestMethod = value == '全部' ? null : HttpMethod.valueOf(value);
|
||||
})),
|
||||
const SizedBox(height: 15),
|
||||
row(
|
||||
const Text('请求类型:'),
|
||||
DropdownMenu(
|
||||
initialValue: Maps.getKey(requestContentMap, searchModel.requestContentType) ?? '全部',
|
||||
items: requestContentMap.keys,
|
||||
onSelected: (String value) {
|
||||
searchModel.requestContentType = requestContentMap[value];
|
||||
})),
|
||||
const SizedBox(height: 15),
|
||||
row(
|
||||
const Text('响应类型:'),
|
||||
DropdownMenu(
|
||||
initialValue: Maps.getKey(responseContentMap, searchModel.responseContentType) ?? '全部',
|
||||
items: responseContentMap.keys,
|
||||
onSelected: (String value) {
|
||||
searchModel.responseContentType = responseContentMap[value];
|
||||
})),
|
||||
row(
|
||||
const Text(' 状态码:'),
|
||||
TextFormField(
|
||||
initialValue: searchModel.statusCode?.toString(),
|
||||
onChanged: (val) {
|
||||
searchModel.statusCode = int.tryParse(val);
|
||||
},
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
LengthLimitingTextInputFormatter(5),
|
||||
FilteringTextInputFormatter.allow(RegExp('[-0-9]'))
|
||||
],
|
||||
decoration: const InputDecoration(
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.all(10),
|
||||
TextFormField(
|
||||
initialValue: searchModel.keyword,
|
||||
onChanged: (val) => searchModel.keyword = val,
|
||||
decoration: const InputDecoration(
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.all(10),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(15))),
|
||||
hintText: '关键词',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
const SizedBox(height: 15),
|
||||
const Text("关键词搜索范围:"),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
options('URL', Option.url),
|
||||
options('请求头', Option.requestHeader),
|
||||
options('请求体', Option.requestBody),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [options('响应头', Option.responseHeader), options('响应体', Option.responseBody)],
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
row(
|
||||
const Text('请求方法:'),
|
||||
DropdownMenu(
|
||||
initialValue: searchModel.requestMethod?.name ?? '全部',
|
||||
items: HttpMethod.values.map((e) => e.name).toList()..insert(0, '全部'),
|
||||
onSelected: (String value) {
|
||||
searchModel.requestMethod = value == '全部' ? null : HttpMethod.valueOf(value);
|
||||
})),
|
||||
const SizedBox(height: 15),
|
||||
row(
|
||||
const Text('请求类型:'),
|
||||
DropdownMenu(
|
||||
initialValue: Maps.getKey(requestContentMap, searchModel.requestContentType) ?? '全部',
|
||||
items: requestContentMap.keys,
|
||||
onSelected: (String value) {
|
||||
searchModel.requestContentType = requestContentMap[value];
|
||||
})),
|
||||
const SizedBox(height: 15),
|
||||
row(
|
||||
const Text('响应类型:'),
|
||||
DropdownMenu(
|
||||
initialValue: Maps.getKey(responseContentMap, searchModel.responseContentType) ?? '全部',
|
||||
items: responseContentMap.keys,
|
||||
onSelected: (String value) {
|
||||
searchModel.responseContentType = responseContentMap[value];
|
||||
})),
|
||||
row(
|
||||
const Text(' 状态码:'),
|
||||
TextFormField(
|
||||
initialValue: searchModel.statusCode?.toString(),
|
||||
onChanged: (val) {
|
||||
searchModel.statusCode = int.tryParse(val);
|
||||
},
|
||||
child: const Text('取消')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
widget.onSearch?.call(SearchModel());
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('清除搜索')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
widget.onSearch?.call(searchModel);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('确定')),
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
LengthLimitingTextInputFormatter(5),
|
||||
FilteringTextInputFormatter.allow(RegExp('[-0-9]'))
|
||||
],
|
||||
decoration: const InputDecoration(
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.all(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('取消')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
widget.onSearch?.call(SearchModel());
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('清除搜索')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
widget.onSearch?.call(searchModel);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('确定')),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
Widget options(String title, Option option) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:network_proxy/ui/launch/launch.dart';
|
||||
import 'package:network_proxy/ui/mobile/connect_remote.dart';
|
||||
import 'package:network_proxy/ui/mobile/menu.dart';
|
||||
import 'package:network_proxy/ui/mobile/request/list.dart';
|
||||
import 'package:network_proxy/ui/mobile/request/search.dart';
|
||||
|
||||
class MobileHomePage extends StatefulWidget {
|
||||
final Configuration configuration;
|
||||
@@ -70,15 +71,19 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: search(), actions: [
|
||||
IconButton(
|
||||
tooltip: "清理",
|
||||
icon: const Icon(Icons.cleaning_services_outlined),
|
||||
onPressed: () => requestStateKey.currentState?.clean()),
|
||||
const SizedBox(width: 2),
|
||||
MoreEnum(proxyServer: proxyServer, desktop: desktop),
|
||||
const SizedBox(width: 10)
|
||||
]),
|
||||
appBar: AppBar(
|
||||
title: MobileSearch(onSearch: (val) {
|
||||
requestStateKey.currentState?.search(val);
|
||||
}),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: "清理",
|
||||
icon: const Icon(Icons.cleaning_services_outlined),
|
||||
onPressed: () => requestStateKey.currentState?.clean()),
|
||||
const SizedBox(width: 2),
|
||||
MoreEnum(proxyServer: proxyServer, desktop: desktop),
|
||||
const SizedBox(width: 10)
|
||||
]),
|
||||
drawer: DrawerWidget(proxyServer: proxyServer),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {},
|
||||
@@ -91,36 +96,39 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener {
|
||||
body: ValueListenableBuilder(
|
||||
valueListenable: desktop,
|
||||
builder: (context, value, _) {
|
||||
|
||||
return Column(children: [
|
||||
value.connect == false
|
||||
? const SizedBox()
|
||||
: Container(
|
||||
margin: const EdgeInsets.only(top: 5, bottom: 5),
|
||||
height: 50,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {
|
||||
return ConnectRemote(desktop: desktop, proxyServer: proxyServer);
|
||||
})),
|
||||
child: Text("已连接${value.os?.toUpperCase()},手机抓包已关闭",
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
)),
|
||||
Expanded(child: RequestListWidget(key: requestStateKey, proxyServer: proxyServer))
|
||||
value.connect ? remoteConnect(value) : const SizedBox(),
|
||||
Expanded(child: RequestListWidget(key: requestStateKey, proxyServer: proxyServer))
|
||||
]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
showUpgradeNotice() {
|
||||
String content = '1. 手机版启动默认不再自动开启抓包,请手动点击启动按钮。\n'
|
||||
'2. 搜索功能增强,可直接搜索响应类型和请求方法。\n'
|
||||
'3. 支持brotli编码,br响应类型编码不会再显示乱码';
|
||||
String content = '1. 增加高级搜索,点击搜索icon触发。\n'
|
||||
'2. 显示SSL握手异常、建立连接异常、未知异常等请求。\n'
|
||||
'3.响应体大时异步加载json,请求重写增加域名,修复手机扫码连接未开启代理时不转发问题';
|
||||
showAlertDialog('更新内容', content, () {
|
||||
widget.configuration.upgradeNotice = false;
|
||||
widget.configuration.flushConfig();
|
||||
});
|
||||
}
|
||||
|
||||
/// 远程连接
|
||||
Widget remoteConnect(RemoteModel value) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 5, bottom: 5),
|
||||
height: 50,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {
|
||||
return ConnectRemote(desktop: desktop, proxyServer: proxyServer);
|
||||
})),
|
||||
child: Text("已连接${value.os?.toUpperCase()},手机抓包已关闭", style: Theme.of(context).textTheme.titleMedium),
|
||||
));
|
||||
}
|
||||
|
||||
showAlertDialog(String title, String content, Function onClose) {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -137,21 +145,6 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener {
|
||||
});
|
||||
}
|
||||
|
||||
/// 搜索框
|
||||
Widget search() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 20),
|
||||
child: TextField(
|
||||
cursorHeight: 20,
|
||||
keyboardType: TextInputType.url,
|
||||
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onChanged: (val) {
|
||||
requestStateKey.currentState?.search(val);
|
||||
},
|
||||
decoration:
|
||||
const InputDecoration(border: InputBorder.none, prefixIcon: Icon(Icons.search), hintText: 'Search')));
|
||||
}
|
||||
|
||||
/// 检查远程连接
|
||||
checkConnectTask(BuildContext context) async {
|
||||
int retry = 0;
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:network_proxy/network/channel.dart';
|
||||
import 'package:network_proxy/network/host_port.dart';
|
||||
import 'package:network_proxy/network/http/http.dart';
|
||||
import 'package:network_proxy/network/util/host_filter.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/model/search_model.dart';
|
||||
import 'package:network_proxy/ui/mobile/request/request.dart';
|
||||
|
||||
class RequestListWidget extends StatefulWidget {
|
||||
@@ -68,9 +69,9 @@ class RequestListState extends State<RequestListWidget> {
|
||||
container.removeWhere((element) => list.contains(element));
|
||||
}
|
||||
|
||||
search(String text) {
|
||||
requestSequenceKey.currentState?.search(text.trim());
|
||||
domainListKey.currentState?.search(text.trim());
|
||||
search(SearchModel searchModel) {
|
||||
requestSequenceKey.currentState?.search(searchModel);
|
||||
domainListKey.currentState?.search(searchModel.keyword?.trim());
|
||||
}
|
||||
|
||||
///清理
|
||||
@@ -107,8 +108,8 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
|
||||
late Queue<HttpRequest> view = Queue();
|
||||
bool changing = false;
|
||||
|
||||
//搜索关键字
|
||||
String? searchText;
|
||||
//搜索的内容
|
||||
SearchModel? searchModel;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
@@ -120,7 +121,9 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
|
||||
///添加请求
|
||||
add(HttpRequest request) {
|
||||
list.add(request);
|
||||
if (!filter(request)) {
|
||||
|
||||
///过滤
|
||||
if (searchModel?.isNotEmpty == true && !searchModel!.filter(request, request.response)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -133,6 +136,21 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
|
||||
response.request?.response = response;
|
||||
var state = indexes.remove(response.request);
|
||||
state?.currentState?.change(response);
|
||||
|
||||
if (searchModel == null || searchModel!.isEmpty || response.request == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
print("object ${searchModel?.filter(response.request!, response) } ${state == null}");
|
||||
//搜索视图
|
||||
if (searchModel?.filter(response.request!, response) == true && state == null) {
|
||||
print("contains ${view.contains(response.request)}");
|
||||
|
||||
if (!view.contains(response.request)) {
|
||||
view.addFirst(response.request!);
|
||||
changeState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clean() {
|
||||
@@ -143,40 +161,17 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
|
||||
});
|
||||
}
|
||||
|
||||
void search(String text) {
|
||||
text = text.toLowerCase();
|
||||
if (text == searchText) {
|
||||
return;
|
||||
}
|
||||
|
||||
//包含从上次结果过滤
|
||||
if (text.contains(searchText ?? "")) {
|
||||
searchText = text;
|
||||
view.retainWhere(filter);
|
||||
///过滤
|
||||
void search(SearchModel searchModel) {
|
||||
this.searchModel = searchModel;
|
||||
if (searchModel.isEmpty) {
|
||||
view = Queue.of(list.reversed);
|
||||
} else {
|
||||
searchText = text;
|
||||
view = Queue.of(list.where(filter).toList().reversed);
|
||||
view = Queue.of(list.where((it) => searchModel.filter(it, it.response)).toList().reversed);
|
||||
}
|
||||
|
||||
changeState();
|
||||
}
|
||||
|
||||
bool filter(HttpRequest request) {
|
||||
if (searchText == null || searchText!.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (request.method.name.toLowerCase() == searchText) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (request.requestUrl.toLowerCase().contains(searchText!)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return request.response?.contentType.name.toLowerCase().contains(searchText!) == true;
|
||||
}
|
||||
|
||||
changeState() {
|
||||
//防止频繁刷新
|
||||
if (!changing) {
|
||||
@@ -239,7 +234,6 @@ class DomainListState extends State<DomainList> with AutomaticKeepAliveClientMix
|
||||
//显示的域名 最新的在顶部
|
||||
List<HostAndPort> list = [];
|
||||
HostAndPort? showHostAndPort;
|
||||
bool changing = false;
|
||||
|
||||
//搜索关键字
|
||||
String? searchText;
|
||||
@@ -282,15 +276,7 @@ class DomainListState extends State<DomainList> with AutomaticKeepAliveClientMix
|
||||
}
|
||||
|
||||
this.list = [...container.where(filter)].reversed.toList();
|
||||
//防止频繁刷新
|
||||
if (!changing) {
|
||||
changing = true;
|
||||
Future.delayed(const Duration(milliseconds: 50), () {
|
||||
setState(() {
|
||||
changing = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
addResponse(HttpResponse response) {
|
||||
@@ -307,10 +293,19 @@ class DomainListState extends State<DomainList> with AutomaticKeepAliveClientMix
|
||||
});
|
||||
}
|
||||
|
||||
void search(String text) {
|
||||
///搜索域名
|
||||
void search(String? text) {
|
||||
if (text == null) {
|
||||
setState(() {
|
||||
list = List.of(container.toList().reversed);
|
||||
searchText = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
text = text.toLowerCase();
|
||||
setState(() {
|
||||
var contains = text.contains(searchText ?? "");
|
||||
var contains = text!.contains(searchText ?? "");
|
||||
searchText = text.toLowerCase();
|
||||
if (contains) {
|
||||
//包含从上次结果过滤
|
||||
@@ -349,8 +344,7 @@ class DomainListState extends State<DomainList> with AutomaticKeepAliveClientMix
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(list.elementAt(index).domain, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
subtitle: Text("最后请求时间: $time, 次数: ${value?.length}",
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Text("最后请求时间: $time, 次数: ${value?.length}", maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
onLongPress: () => menu(index),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
|
||||
77
lib/ui/mobile/request/search.dart
Normal file
77
lib/ui/mobile/request/search.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/model/search_model.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/search_condition.dart';
|
||||
|
||||
class MobileSearch extends StatefulWidget {
|
||||
final Function(SearchModel searchModel)? onSearch;
|
||||
|
||||
const MobileSearch({super.key, this.onSearch});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return MobileSearchState();
|
||||
}
|
||||
}
|
||||
|
||||
class MobileSearchState extends State<MobileSearch> {
|
||||
SearchModel searchModel = SearchModel();
|
||||
bool searched = false;
|
||||
TextEditingController keywordController = TextEditingController();
|
||||
bool changing = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 20),
|
||||
child: TextFormField(
|
||||
controller: keywordController,
|
||||
cursorHeight: 20,
|
||||
keyboardType: TextInputType.url,
|
||||
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onChanged: (val) {
|
||||
searchModel.keyword = val;
|
||||
if (!changing) {
|
||||
changing = true;
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
changing = false;
|
||||
if (!searched) {
|
||||
searchModel.searchOptions = {Option.url, Option.method, Option.responseContentType};
|
||||
}
|
||||
widget.onSearch?.call(searchModel);
|
||||
});
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
prefixIcon:
|
||||
InkWell(onTap: showSearch, child: Icon(Icons.search, color: searched ? Colors.green : Colors.blue)),
|
||||
hintText: 'Search')));
|
||||
}
|
||||
|
||||
showSearch() {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
if (!searched) {
|
||||
searchModel.searchOptions = {Option.url};
|
||||
}
|
||||
return Padding(
|
||||
padding: MediaQuery.of(context).viewInsets,
|
||||
child: SizedBox(
|
||||
height: 430,
|
||||
child: SearchConditions(
|
||||
padding: const EdgeInsets.only(left: 15, right: 15, top: 10),
|
||||
searchModel: searchModel,
|
||||
onSearch: (val) {
|
||||
setState(() {
|
||||
searchModel = val;
|
||||
searched = searchModel.isNotEmpty;
|
||||
keywordController.text = searchModel.keyword ?? '';
|
||||
widget.onSearch?.call(searchModel);
|
||||
});
|
||||
},
|
||||
)));
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user