手机端高级搜索

This commit is contained in:
wanghongen
2023-08-07 14:18:54 +08:00
parent 26511025cd
commit c7f7a645e1
11 changed files with 431 additions and 373 deletions

View File

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

View File

@@ -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';

View File

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

View File

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

View 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,
}

View File

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

View File

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

View File

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

View File

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

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