From 799008fe476a50da20cc5ede71bc6ed3355e1047 Mon Sep 17 00:00:00 2001 From: wanghongenpin Date: Fri, 8 Nov 2024 19:05:56 +0800 Subject: [PATCH] Desktop toolbox timestamp --- README.md | 4 +- README_CN.md | 2 + lib/l10n/app_en.arb | 7 +- lib/l10n/app_zh.arb | 7 +- lib/network/http/content_type.dart | 3 +- lib/ui/component/multi_window.dart | 16 +- lib/ui/component/{ => toolbox}/cert_hash.dart | 2 +- lib/ui/component/{ => toolbox}/encoder.dart | 0 lib/ui/component/{ => toolbox}/js_run.dart | 2 +- .../component/{ => toolbox}/qr_code_page.dart | 0 lib/ui/component/{ => toolbox}/regexp.dart | 0 lib/ui/component/toolbox/timestamp.dart | 164 ++++++ lib/ui/component/{ => toolbox}/toolbox.dart | 25 +- lib/ui/content/body.dart | 2 +- lib/ui/desktop/desktop.dart | 2 +- lib/ui/desktop/request/domians.dart | 543 ++++++++++++++++++ lib/ui/desktop/request/list.dart | 492 +--------------- lib/ui/desktop/request/request.dart | 113 +--- lib/ui/desktop/request/request_sequence.dart | 8 +- lib/ui/desktop/widgets/highlight.dart | 106 ++++ lib/ui/mobile/menu/drawer.dart | 2 +- lib/ui/mobile/mobile.dart | 2 +- pubspec.yaml | 4 +- 23 files changed, 885 insertions(+), 621 deletions(-) rename lib/ui/component/{ => toolbox}/cert_hash.dart (97%) rename lib/ui/component/{ => toolbox}/encoder.dart (100%) rename lib/ui/component/{ => toolbox}/js_run.dart (98%) rename lib/ui/component/{ => toolbox}/qr_code_page.dart (100%) rename lib/ui/component/{ => toolbox}/regexp.dart (100%) create mode 100644 lib/ui/component/toolbox/timestamp.dart rename lib/ui/component/{ => toolbox}/toolbox.dart (88%) create mode 100644 lib/ui/desktop/request/domians.dart create mode 100644 lib/ui/desktop/widgets/highlight.dart diff --git a/README.md b/README.md index 5a46d5f..01ea411 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ProxyPin English | [中文](README_CN.md) -## Open source free packet capture tool,Support Windows、Mac、Android、IOS、Linux Full platform system +## Open source free traffic capture HTTP(S),Support Windows、Mac、Android、IOS、Linux Full platform system You can use it to intercept, inspect & rewrite HTTP(S) traffic, Support capturing Flutter app traffic, ProxyPin is based on Flutter develop, and the UI is beautiful and easy to use. @@ -22,6 +22,8 @@ Download: https://github.com/wanghongenpin/proxypin/releases iOS App Store:https://apps.apple.com/app/proxypin/id6450932949 +Android Google Play:https://play.google.com/store/apps/details?id=com.network.proxy + TG: https://t.me/proxypin_en **We will continue to improve the features and experience, as well as optimize the UI.** diff --git a/README_CN.md b/README_CN.md index 70c4bb7..acc9d49 100644 --- a/README_CN.md +++ b/README_CN.md @@ -22,6 +22,8 @@ iOS AppStore下载地址: https://apps.apple.com/app/proxypin/id6450932949 +Android Google Play:https://play.google.com/store/apps/details?id=com.network.proxy + TG: https://t.me/proxypin_tg **接下来会持续完善功能和体验,UI优化。** diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5afbb70..eb71f07 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -304,5 +304,10 @@ "saveImage": "Save Image", "selectImage": "Select Image", "inputContent": "Input Content", - "errorCorrectLevel": "Error Correct" + "errorCorrectLevel": "Error Correct", + "output": "Output", + "timestamp": "Timestamp", + "convert": "Convert", + "time": "DateTime", + "nowTimestamp": "Now timestamp" } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index d736c36..212d269 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -303,5 +303,10 @@ "saveImage": "保存图片", "selectImage": "选择图片", "inputContent": "输入内容", - "errorCorrectLevel": "纠错等级" + "errorCorrectLevel": "纠错等级", + "output": "输出", + "timestamp": "时间戳", + "convert": "转换", + "time": "时间", + "nowTimestamp": "当前时间戳(秒)" } \ No newline at end of file diff --git a/lib/network/http/content_type.dart b/lib/network/http/content_type.dart index 732cd6f..372d2a5 100644 --- a/lib/network/http/content_type.dart +++ b/lib/network/http/content_type.dart @@ -125,7 +125,7 @@ class MediaType { int nextIndex = index + 1; bool quoted = false; while (nextIndex < mediaType.length) { - var ch = mediaType[0]; + var ch = mediaType[nextIndex]; if (ch == ';') { if (!quoted) { break; @@ -135,6 +135,7 @@ class MediaType { } nextIndex++; } + String parameter = mediaType.substring(index + 1, nextIndex).trim(); if (parameter.isNotEmpty) { int eqIndex = parameter.indexOf('='); diff --git a/lib/ui/component/multi_window.dart b/lib/ui/component/multi_window.dart index 9a26903..2cca3fe 100644 --- a/lib/ui/component/multi_window.dart +++ b/lib/ui/component/multi_window.dart @@ -27,12 +27,12 @@ import 'package:proxypin/network/components/manager/rewrite_rule.dart'; import 'package:proxypin/network/components/manager/script_manager.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/util/logger.dart'; -import 'package:proxypin/ui/component/cert_hash.dart'; +import 'package:proxypin/ui/component/toolbox/cert_hash.dart'; import 'package:proxypin/ui/component/device.dart'; -import 'package:proxypin/ui/component/encoder.dart'; -import 'package:proxypin/ui/component/js_run.dart'; -import 'package:proxypin/ui/component/qr_code_page.dart'; -import 'package:proxypin/ui/component/regexp.dart'; +import 'package:proxypin/ui/component/toolbox/encoder.dart'; +import 'package:proxypin/ui/component/toolbox/js_run.dart'; +import 'package:proxypin/ui/component/toolbox/qr_code_page.dart'; +import 'package:proxypin/ui/component/toolbox/regexp.dart'; import 'package:proxypin/ui/component/utils.dart'; import 'package:proxypin/ui/content/body.dart'; import 'package:proxypin/ui/desktop/request/request_editor.dart'; @@ -42,6 +42,8 @@ import 'package:proxypin/utils/platform.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:window_manager/window_manager.dart'; +import 'toolbox/timestamp.dart'; + bool isMultiWindow = false; ///多窗口 @@ -93,6 +95,10 @@ Widget multiWindow(int windowId, Map argument) { if (argument['name'] == 'RegExpPage') { return const RegExpPage(); } + if (argument['name'] == 'TimestampPage') { + return TimestampPage(windowId: windowId); + } + //脚本日志 if (argument['name'] == 'ScriptConsoleWidget') { return ScriptConsoleWidget(windowId: windowId); diff --git a/lib/ui/component/cert_hash.dart b/lib/ui/component/toolbox/cert_hash.dart similarity index 97% rename from lib/ui/component/cert_hash.dart rename to lib/ui/component/toolbox/cert_hash.dart index 5d3ceed..dc5f815 100644 --- a/lib/ui/component/cert_hash.dart +++ b/lib/ui/component/toolbox/cert_hash.dart @@ -98,7 +98,7 @@ class _CertHashPageState extends State { decoration: decoration(context, label: localizations.inputContent))), Align( alignment: Alignment.bottomLeft, - child: TextButton(onPressed: () {}, child: const Text("Output:", style: TextStyle(fontSize: 16)))), + child: TextButton(onPressed: () {}, child: Text("${localizations.output}:", style: TextStyle(fontSize: 16)))), Container( width: double.infinity, padding: const EdgeInsets.all(10), diff --git a/lib/ui/component/encoder.dart b/lib/ui/component/toolbox/encoder.dart similarity index 100% rename from lib/ui/component/encoder.dart rename to lib/ui/component/toolbox/encoder.dart diff --git a/lib/ui/component/js_run.dart b/lib/ui/component/toolbox/js_run.dart similarity index 98% rename from lib/ui/component/js_run.dart rename to lib/ui/component/toolbox/js_run.dart index 50a38e8..b110966 100644 --- a/lib/ui/component/js_run.dart +++ b/lib/ui/component/toolbox/js_run.dart @@ -145,7 +145,7 @@ class _JavaScriptState extends State { ))))), const SizedBox(height: 10), Row(children: [ - Text("Output:", style: TextStyle(fontSize: 16, color: primaryColor, fontWeight: FontWeight.w500)), + Text("${localizations.output}:", style: TextStyle(fontSize: 16, color: primaryColor, fontWeight: FontWeight.w500)), const SizedBox(width: 15), //copy IconButton( diff --git a/lib/ui/component/qr_code_page.dart b/lib/ui/component/toolbox/qr_code_page.dart similarity index 100% rename from lib/ui/component/qr_code_page.dart rename to lib/ui/component/toolbox/qr_code_page.dart diff --git a/lib/ui/component/regexp.dart b/lib/ui/component/toolbox/regexp.dart similarity index 100% rename from lib/ui/component/regexp.dart rename to lib/ui/component/toolbox/regexp.dart diff --git a/lib/ui/component/toolbox/timestamp.dart b/lib/ui/component/toolbox/timestamp.dart new file mode 100644 index 0000000..c016dd3 --- /dev/null +++ b/lib/ui/component/toolbox/timestamp.dart @@ -0,0 +1,164 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_toastr/flutter_toastr.dart'; +import 'package:proxypin/utils/lang.dart'; + +import '../text_field.dart'; + +/// Timestamp page +/// @author Hongen Wang +class TimestampPage extends StatefulWidget { + final int? windowId; + + const TimestampPage({super.key, this.windowId}); + + @override + State createState() { + return _TimestampPageState(); + } +} + +class _TimestampPageState extends State { + AppLocalizations get localizations => AppLocalizations.of(context)!; + + TextEditingController nowTimestamp = TextEditingController(); + TextEditingController timestamp = TextEditingController(); + TextEditingController dateTime = TextEditingController(); + + TextEditingController timestampOut = TextEditingController(); + TextEditingController dateTimeOut = TextEditingController(); + + ButtonStyle get buttonStyle => ButtonStyle( + padding: WidgetStateProperty.all(EdgeInsets.symmetric(horizontal: 15, vertical: 8)), + // textStyle: WidgetStateProperty.all(TextStyle(fontSize: 14)), + shape: WidgetStateProperty.all( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)))); + + @override + void initState() { + super.initState(); + + nowTimestamp.text = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(); + timestamp.text = nowTimestamp.text; + dateTime.text = DateTime.now().format(); + //定时器 + Timer.periodic(Duration(seconds: 1), (timer) { + if (!mounted) timer.cancel(); + nowTimestamp.text = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(); + }); + } + + @override + void dispose() { + nowTimestamp.dispose(); + timestamp.dispose(); + dateTime.dispose(); + timestampOut.dispose(); + dateTimeOut.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + var textStyle = Theme.of(context).textTheme.titleMedium; + + return Scaffold( + appBar: AppBar(title: Text(localizations.timestamp, style: TextStyle(fontSize: 16)), centerTitle: true), + body: ListView( + padding: const EdgeInsets.symmetric(horizontal: 10), + children: [ + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text('${localizations.nowTimestamp}:', style: textStyle), + const SizedBox(width: 6), + SizedBox( + width: 100, + child: TextField( + controller: nowTimestamp, readOnly: true, decoration: InputDecoration(border: InputBorder.none))), + IconButton( + icon: Icon(Icons.copy, size: 18), + onPressed: () { + Clipboard.setData(ClipboardData(text: nowTimestamp.text)); + FlutterToastr.show(localizations.copied, context); + }, + ), + ], + ), + SizedBox(height: 15), + Wrap( + spacing: 10.0, runSpacing: 10.0, crossAxisAlignment: WrapCrossAlignment.center, + children: [ + SizedBox(width: 93, child: Text('${localizations.timestamp}:', style: textStyle)), + SizedBox( + width: 215, + child: TextFormField(controller: timestamp, decoration: decoration(context, hintText: 'timestamp'))), + FilledButton.icon( + icon: Icon(Icons.play_arrow_rounded), + style: buttonStyle, + label: Text(localizations.convert), + onPressed: () => timestampConvert(timestamp.text)), + SizedBox( + width: 200, + child: TextFormField( + controller: timestampOut, + readOnly: true, + decoration: InputDecoration(border: OutlineInputBorder())), + ), + ], + ), + SizedBox(height: 35), + Wrap(spacing: 10.0, runSpacing: 10.0, crossAxisAlignment: WrapCrossAlignment.center, children: [ + SizedBox(width: 93, child: Text('${localizations.time}:', style: textStyle)), + SizedBox( + width: 215, + child: TextFormField( + controller: dateTime, decoration: decoration(context, hintText: 'yyyy-MM-dd HH:mm:ss'))), + FilledButton.icon( + icon: Icon(Icons.play_arrow_rounded), + style: buttonStyle, + label: Text(localizations.convert), + onPressed: () => timeConvert(dateTime.text)), + SizedBox( + width: 200, + child: TextFormField( + controller: dateTimeOut, + readOnly: true, + decoration: InputDecoration(border: OutlineInputBorder()))), + ]), + ], + ), + ); + } + + timestampConvert(String timestamp) { + if (timestamp.isEmpty) return; + try { + if (timestamp.length == 13) { + timestampOut.text = DateTime.fromMillisecondsSinceEpoch(int.parse(timestamp)).format(); + return; + } + + if (timestamp.length == 10) { + timestampOut.text = DateTime.fromMillisecondsSinceEpoch(int.parse(timestamp) * 1000).format(); + return; + } + FlutterToastr.show('Invalid timestamp', context); + } catch (e) { + FlutterToastr.show('Invalid timestamp', context); + } + } + + timeConvert(String dateTime) { + if (dateTime.isEmpty) return; + try { + var date = DateTime.parse(dateTime); + dateTimeOut.text = (date.millisecondsSinceEpoch ~/ 1000).toString(); + } catch (e) { + FlutterToastr.show('Invalid date time', context); + } + } +} diff --git a/lib/ui/component/toolbox.dart b/lib/ui/component/toolbox/toolbox.dart similarity index 88% rename from lib/ui/component/toolbox.dart rename to lib/ui/component/toolbox/toolbox.dart index e659a27..0bb65c9 100644 --- a/lib/ui/component/toolbox.dart +++ b/lib/ui/component/toolbox/toolbox.dart @@ -5,12 +5,13 @@ import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:proxypin/network/bin/server.dart'; -import 'package:proxypin/ui/component/cert_hash.dart'; -import 'package:proxypin/ui/component/encoder.dart'; -import 'package:proxypin/ui/component/js_run.dart'; +import 'package:proxypin/ui/component/toolbox/cert_hash.dart'; +import 'package:proxypin/ui/component/toolbox/encoder.dart'; +import 'package:proxypin/ui/component/toolbox/js_run.dart'; import 'package:proxypin/ui/component/multi_window.dart'; -import 'package:proxypin/ui/component/qr_code_page.dart'; -import 'package:proxypin/ui/component/regexp.dart'; +import 'package:proxypin/ui/component/toolbox/qr_code_page.dart'; +import 'package:proxypin/ui/component/toolbox/regexp.dart'; +import 'package:proxypin/ui/component/toolbox/timestamp.dart'; import 'package:proxypin/ui/mobile/request/request_editor.dart'; import 'package:proxypin/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; @@ -111,8 +112,20 @@ class _ToolboxState extends State { ), const Divider(thickness: 0.3), Text(localizations.other, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - Row( + Wrap( children: [ + IconText( + onTap: () async { + if (Platforms.isMobile()) { + Navigator.of(context).push(MaterialPageRoute(builder: (context) => const TimestampPage())); + return; + } + + MultiWindow.openWindow(localizations.timestamp, 'TimestampPage', size: const Size(650, 330)); + }, + icon: Icons.av_timer, + text: localizations.timestamp), + const SizedBox(width: 10), IconText( onTap: () async { if (Platforms.isMobile()) { diff --git a/lib/ui/content/body.dart b/lib/ui/content/body.dart index 28fa8f8..2e78cda 100644 --- a/lib/ui/content/body.dart +++ b/lib/ui/content/body.dart @@ -26,7 +26,7 @@ import 'package:proxypin/network/components/manager/rewrite_rule.dart'; import 'package:proxypin/network/http/content_type.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/util/logger.dart'; -import 'package:proxypin/ui/component/encoder.dart'; +import 'package:proxypin/ui/component/toolbox/encoder.dart'; import 'package:proxypin/ui/component/json/json_viewer.dart'; import 'package:proxypin/ui/component/json/theme.dart'; import 'package:proxypin/ui/component/multi_window.dart'; diff --git a/lib/ui/desktop/desktop.dart b/lib/ui/desktop/desktop.dart index a1fbcee..88a24d1 100644 --- a/lib/ui/desktop/desktop.dart +++ b/lib/ui/desktop/desktop.dart @@ -23,7 +23,7 @@ import 'package:proxypin/network/handler.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/websocket.dart'; import 'package:proxypin/ui/component/memory_cleanup.dart'; -import 'package:proxypin/ui/component/toolbox.dart'; +import 'package:proxypin/ui/component/toolbox/toolbox.dart'; import 'package:proxypin/ui/component/widgets.dart'; import 'package:proxypin/ui/configuration.dart'; import 'package:proxypin/ui/content/panel.dart'; diff --git a/lib/ui/desktop/request/domians.dart b/lib/ui/desktop/request/domians.dart new file mode 100644 index 0000000..889c260 --- /dev/null +++ b/lib/ui/desktop/request/domians.dart @@ -0,0 +1,543 @@ +/* + * Copyright 2023 Hongen Wang All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'dart:collection'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_desktop_context_menu/flutter_desktop_context_menu.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_toastr/flutter_toastr.dart'; +import 'package:proxypin/network/bin/configuration.dart'; +import 'package:proxypin/network/bin/server.dart'; +import 'package:proxypin/network/channel.dart'; +import 'package:proxypin/network/components/host_filter.dart'; +import 'package:proxypin/network/host_port.dart'; +import 'package:proxypin/network/http/http.dart'; +import 'package:proxypin/network/http_client.dart'; +import 'package:proxypin/ui/component/transition.dart'; +import 'package:proxypin/ui/component/utils.dart'; +import 'package:proxypin/ui/content/panel.dart'; +import 'package:proxypin/ui/desktop/request/model/search_model.dart'; +import 'package:proxypin/ui/desktop/request/request.dart'; +import 'package:proxypin/ui/desktop/widgets/highlight.dart'; +import 'package:proxypin/utils/listenable_list.dart'; + +/// 左侧域名 +/// @author wanghongen +/// 2023/10/8 +class DomainList extends StatefulWidget { + final ProxyServer proxyServer; + final NetworkTabController panel; + + final ListenableList list; + final bool shrinkWrap; + final Function(List)? onRemove; + + const DomainList( + {super.key, + required this.proxyServer, + required this.list, + this.shrinkWrap = true, + required this.panel, + this.onRemove}); + + @override + State createState() { + return DomainWidgetState(); + } +} + +class DomainWidgetState extends State with AutomaticKeepAliveClientMixin { + //域名和对应请求列表的映射 + final LinkedHashMap containerMap = LinkedHashMap(); + + //搜索视图 + LinkedHashMap searchView = LinkedHashMap(); + + //搜索的内容 + SearchModel? searchModel; + bool changing = false; //是否存在刷新任务 + //关键词高亮监听 + late VoidCallback highlightListener; + + changeState() { + if (!changing) { + changing = true; + Future.delayed(const Duration(milliseconds: 500), () { + setState(() { + changing = false; + }); + }); + } + } + + @override + void initState() { + super.initState(); + var container = widget.list; + for (var request in container.source) { + DomainRequests domainRequests = getDomainRequests(request); + domainRequests.addRequest(request.requestId, request); + } + highlightListener = () { + //回调时机在高亮设置页面dispose之后。所以需要在下一帧刷新,否则会报错 + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + highlightHandler(); + }); + }; + DesktopKeywordHighlight.keywordsController.addListener(highlightListener); + } + + @override + dispose() { + DesktopKeywordHighlight.keywordsController.removeListener(highlightListener); + super.dispose(); + } + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + var list = containerMap.values; + + //根究搜素文本过滤 + if (searchModel?.isNotEmpty == true) { + searchView = searchFilter(searchModel!); + list = searchView.values; + } else { + searchView.clear(); + } + + return widget.shrinkWrap + ? SingleChildScrollView(child: Column(children: list.toList())) + : ListView.builder(itemCount: list.length, itemBuilder: (_, index) => list.elementAt(index)); + } + + ///搜索 + void search(SearchModel? val) { + setState(() { + searchModel = val; + }); + } + + ///搜索过滤 + LinkedHashMap searchFilter(SearchModel searchModel) { + LinkedHashMap result = LinkedHashMap(); + + containerMap.forEach((key, domainRequests) { + var body = domainRequests.search(searchModel); + if (body.isNotEmpty) { + result[key] = domainRequests.copy(body: body, selected: searchView[key]?.currentSelected); + } + }); + + return result; + } + + ///高亮处理 + highlightHandler() { + //获取所有请求Widget + List requests = containerMap.values.map((e) => e.body).expand((element) => element).toList(); + for (RequestWidget request in requests) { + GlobalKey key = request.key as GlobalKey; + key.currentState?.setState(() {}); + } + } + + ///添加请求 + add(Channel channel, HttpRequest request) { + String? host = request.remoteDomain(); + if (host == null) { + return; + } + + //按照域名分类 + DomainRequests domainRequests = getDomainRequests(request); + var isNew = domainRequests.body.isEmpty; + + domainRequests.addRequest(request.requestId, request); + //搜索视图 + if (searchModel?.isNotEmpty == true && searchModel?.filter(request, null) == true) { + searchView[host]?.addRequest(request.requestId, request); + } + + if (isNew) { + setState(() { + containerMap[host] = domainRequests; + }); + } + } + + DomainRequests getDomainRequests(HttpRequest request) { + var host = request.remoteDomain()!; + DomainRequests? domainRequests = containerMap[host]; + if (domainRequests == null) { + domainRequests = DomainRequests( + host, + proxyServer: widget.panel.proxyServer, + trailing: appIcon(request), + onDelete: deleteHost, + onRequestRemove: (req) { + widget.onRemove?.call([req]); + changeState(); + }, + ); + containerMap[host] = domainRequests; + } + + return domainRequests; + } + + Widget? appIcon(HttpRequest request) { + var processInfo = request.processInfo; + if (processInfo == null) { + return null; + } + + return futureWidget( + processInfo.getIcon(), + (data) => + data.isEmpty ? const SizedBox() : Image.memory(data, width: 23, height: Platform.isWindows ? 16 : null)); + } + + ///移除域名 + deleteHost(String host) { + DomainRequests? domainRequests = containerMap.remove(host); + if (domainRequests == null) { + return; + } + setState(() {}); + + widget.onRemove?.call(domainRequests.body.map((e) => e.request).toList()); + } + + ///添加响应 + addResponse(ChannelContext channelContext, HttpResponse response) { + String domain = channelContext.host!.domain; + DomainRequests? domainRequests = containerMap[domain]; + var pathRow = domainRequests?.getRequest(response); + pathRow?.setResponse(response); + if (pathRow == null) { + return; + } + + //搜索视图 + if (searchModel?.isNotEmpty == true && searchModel?.filter(pathRow.request, response) == true) { + var requests = searchView[domain]; + if (requests?.getRequest(response) == null) { + requests?.addRequest(response.requestId, pathRow.request); + } + requests?.getRequest(response)?.setResponse(response); + } + } + + remove(List list) { + for (var request in list) { + String? host = request.remoteDomain(); + containerMap[host]?._removeRequest(request); + } + } + + ///清理 + clean() { + setState(() { + containerMap.clear(); + searchView.clear(); + + var container = widget.list; + for (var request in container.source) { + DomainRequests domainRequests = getDomainRequests(request); + domainRequests.addRequest(request.requestId, request); + } + }); + } + + List currentView() { + var container = containerMap.values; + if (searchModel?.isNotEmpty == true) { + container = searchView.values; + } + return container.expand((list) => list.body.map((it) => it.request)).toList(); + } +} + +///标题和内容布局 标题是域名 内容是域名下请求 +class DomainRequests extends StatefulWidget { + //请求ID和请求的映射 + final Map requestMap = HashMap(); + + final String domain; + final ProxyServer proxyServer; + final Widget? trailing; + + //请求列表 + final Queue body = Queue(); + + //是否选中 + final bool selected; + + //移除回调 + final Function(String host)? onDelete; + final Function(HttpRequest request)? onRequestRemove; + + DomainRequests(this.domain, + {this.selected = false, this.onDelete, required this.proxyServer, this.onRequestRemove, this.trailing}) + : super(key: GlobalKey<_DomainRequestsState>()); + + ///添加请求 + void addRequest(String? requestId, HttpRequest request) { + if (requestMap.containsKey(requestId)) return; + + var requestWidget = RequestWidget(request, + index: body.length, proxyServer: proxyServer, displayDomain: false, remove: (it) => _remove(it)); + body.addFirst(requestWidget); + if (requestId == null) { + return; + } + requestMap[requestId] = requestWidget; + changeState(); + } + + RequestWidget? getRequest(HttpResponse response) { + return requestMap[response.request?.requestId ?? response.requestId]; + } + + setTrailing(Widget? trailing) { + var state = key as GlobalKey<_DomainRequestsState>; + state.currentState?.trailing = trailing; + } + + _remove(RequestWidget requestWidget) { + if (body.remove(requestWidget)) { + onRequestRemove?.call(requestWidget.request); + changeState(); + } + } + + _removeRequest(HttpRequest request) { + var requestWidget = requestMap.remove(request.requestId); + if (requestWidget != null) { + _remove(requestWidget); + } + } + + ///根据文本过滤 + Iterable search(SearchModel searchModel) { + return body + .where((element) => searchModel.filter(element.request, element.response.get() ?? element.request.response)); + } + + ///复制 + DomainRequests copy({Iterable? body, bool? selected}) { + var state = key as GlobalKey<_DomainRequestsState>; + var headerBody = DomainRequests(domain, + trailing: trailing, + selected: selected ?? state.currentState?.selected == true, + onDelete: onDelete, + onRequestRemove: onRequestRemove, + proxyServer: proxyServer); + if (body != null) { + headerBody.body.addAll(body); + } + return headerBody; + } + + bool get currentSelected { + var state = key as GlobalKey<_DomainRequestsState>; + return state.currentState?.selected == true; + } + + changeState() { + var state = key as GlobalKey<_DomainRequestsState>; + state.currentState?.changeState(); + } + + @override + State createState() { + return _DomainRequestsState(); + } +} + +class _DomainRequestsState extends State { + final GlobalKey transitionState = GlobalKey(); + late Configuration configuration; + late bool selected; + Widget? trailing; + bool changing = false; + + AppLocalizations get localizations => AppLocalizations.of(context)!; + + @override + void initState() { + super.initState(); + configuration = widget.proxyServer.configuration; + selected = widget.selected; + trailing = widget.trailing; + } + + changeState() { + //防止频繁刷新 + if (!changing) { + changing = true; + Future.delayed(const Duration(milliseconds: 500), () { + setState(() { + changing = false; + }); + transitionState.currentState?.show(); + }); + } + } + + @override + Widget build(BuildContext context) { + return Column(children: [ + _hostWidget(widget.domain), + Offstage(offstage: !selected, child: Column(children: widget.body.toList())) + ]); + } + + //domain title + Widget _hostWidget(String title) { + var host = GestureDetector( + onSecondaryTap: menu, + child: ListTile( + minLeadingWidth: 25, + leading: Icon(selected ? Icons.arrow_drop_down : Icons.arrow_right, size: 18), + trailing: trailing, + dense: true, + horizontalTitleGap: 0, + contentPadding: const EdgeInsets.only(left: 3, right: 8), + visualDensity: const VisualDensity(vertical: -3.6), + title: Text(title, + textAlign: TextAlign.left, + style: const TextStyle(fontSize: 14), + maxLines: 1, + overflow: TextOverflow.ellipsis), + onTap: () { + setState(() { + selected = !selected; + }); + })); + + return ColorTransition( + key: transitionState, + duration: const Duration(milliseconds: 1800), + begin: Theme.of(context).focusColor, + startAnimation: false, + child: host); + } + + //域名右键菜单 + menu() { + Menu menu = Menu(items: [ + MenuItem( + label: localizations.copyHost, + onClick: (_) { + Clipboard.setData(ClipboardData(text: Uri.parse(widget.domain).host)); + FlutterToastr.show(localizations.copied, context); + }), + MenuItem.separator(), + MenuItem( + label: localizations.domainFilter, + type: 'submenu', + submenu: hostFilterMenu(), + ), + MenuItem.separator(), + MenuItem(label: localizations.repeatDomainRequests, onClick: (_) => repeatDomainRequests()), + MenuItem.separator(), + MenuItem(label: localizations.delete, onClick: (_) => _delete()), + ]); + + popUpContextMenu(menu); + } + + //重复域名下请求 + void repeatDomainRequests() async { + var list = widget.body.toList().reversed; + for (var requestWidget in list) { + var request = requestWidget.request.copy(uri: requestWidget.request.requestUrl); + var proxyInfo = widget.proxyServer.isRunning ? ProxyInfo.of("127.0.0.1", widget.proxyServer.port) : null; + try { + await HttpClients.proxyRequest(request, proxyInfo: proxyInfo, timeout: const Duration(seconds: 3)); + if (mounted) FlutterToastr.show(localizations.reSendRequest, rootNavigator: true, context); + } catch (e) { + if (mounted) FlutterToastr.show('${localizations.fail}$e', rootNavigator: true, context); + } + } + } + + Menu hostFilterMenu() { + return Menu(items: [ + MenuItem( + label: localizations.domainBlacklist, + onClick: (_) { + HostFilter.blacklist.add(Uri.parse(widget.domain).host); + configuration.flushConfig(); + FlutterToastr.show(localizations.addSuccess, context); + }), + MenuItem( + label: localizations.domainWhitelist, + onClick: (_) { + HostFilter.whitelist.add(Uri.parse(widget.domain).host); + configuration.flushConfig(); + FlutterToastr.show(localizations.addSuccess, context); + }), + MenuItem( + label: localizations.deleteWhitelist, + onClick: (_) { + HostFilter.whitelist.remove(Uri.parse(widget.domain).host); + configuration.flushConfig(); + FlutterToastr.show(localizations.deleteSuccess, context); + }), + ]); + } + + _delete() { + widget.onDelete?.call(widget.domain); + widget.requestMap.clear(); + widget.body.clear(); + FlutterToastr.show(localizations.deleteSuccess, context); + } +} + +class HostWidget extends StatelessWidget { + final String host; + final Function()? onMenu; + + HostWidget(this.host, {this.onMenu}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onSecondaryTap: onMenu, + child: ListTile( + minLeadingWidth: 25, + leading: const Icon(Icons.arrow_right, size: 18), + dense: true, + horizontalTitleGap: 0, + contentPadding: const EdgeInsets.only(left: 3, right: 8), + visualDensity: const VisualDensity(vertical: -3.6), + title: Text(host, + textAlign: TextAlign.left, + style: const TextStyle(fontSize: 14), + maxLines: 1, + overflow: TextOverflow.ellipsis))); + } +} diff --git a/lib/ui/desktop/request/list.dart b/lib/ui/desktop/request/list.dart index 14f88cf..c3bd241 100644 --- a/lib/ui/desktop/request/list.dart +++ b/lib/ui/desktop/request/list.dart @@ -13,32 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import 'dart:collection'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_desktop_context_menu/flutter_desktop_context_menu.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; -import 'package:proxypin/network/bin/configuration.dart'; import 'package:proxypin/network/bin/server.dart'; import 'package:proxypin/network/channel.dart'; -import 'package:proxypin/network/components/host_filter.dart'; -import 'package:proxypin/network/host_port.dart'; import 'package:proxypin/network/http/http.dart'; -import 'package:proxypin/network/http_client.dart'; -import 'package:proxypin/ui/component/transition.dart'; -import 'package:proxypin/ui/component/utils.dart'; import 'package:proxypin/ui/content/panel.dart'; import 'package:proxypin/ui/desktop/request/model/search_model.dart'; -import 'package:proxypin/ui/desktop/request/request.dart'; import 'package:proxypin/ui/desktop/request/request_sequence.dart'; import 'package:proxypin/ui/desktop/request/search.dart'; import 'package:proxypin/utils/har.dart'; import 'package:proxypin/utils/listenable_list.dart'; +import 'domians.dart'; + /// @author wanghongen class DesktopRequestListWidget extends StatefulWidget { final ProxyServer proxyServer; @@ -185,483 +177,3 @@ class DesktopRequestListState extends State with Autom if (mounted) FlutterToastr.show(AppLocalizations.of(context)!.exportSuccess, context); } } - -/// 左侧域名 -/// @author wanghongen -/// 2023/10/8 -class DomainList extends StatefulWidget { - final ProxyServer proxyServer; - final NetworkTabController panel; - - final ListenableList list; - final bool shrinkWrap; - final Function(List)? onRemove; - - const DomainList( - {super.key, - required this.proxyServer, - required this.list, - this.shrinkWrap = true, - required this.panel, - this.onRemove}); - - @override - State createState() { - return DomainWidgetState(); - } -} - -class DomainWidgetState extends State with AutomaticKeepAliveClientMixin { - //域名和对应请求列表的映射 - final LinkedHashMap containerMap = LinkedHashMap(); - - //搜索视图 - LinkedHashMap searchView = LinkedHashMap(); - - //搜索的内容 - SearchModel? searchModel; - bool changing = false; //是否存在刷新任务 - //关键词高亮监听 - late VoidCallback highlightListener; - - changeState() { - if (!changing) { - changing = true; - Future.delayed(const Duration(milliseconds: 500), () { - setState(() { - changing = false; - }); - }); - } - } - - @override - void initState() { - super.initState(); - var container = widget.list; - for (var request in container.source) { - DomainRequests domainRequests = getDomainRequests(request); - domainRequests.addRequest(request.requestId, request); - } - highlightListener = () { - //回调时机在高亮设置页面dispose之后。所以需要在下一帧刷新,否则会报错 - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - highlightHandler(); - }); - }; - KeywordHighlightDialog.keywordsController.addListener(highlightListener); - } - - @override - dispose() { - KeywordHighlightDialog.keywordsController.removeListener(highlightListener); - super.dispose(); - } - - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - var list = containerMap.values; - - //根究搜素文本过滤 - if (searchModel?.isNotEmpty == true) { - searchView = searchFilter(searchModel!); - list = searchView.values; - } else { - searchView.clear(); - } - - return widget.shrinkWrap - ? SingleChildScrollView(child: Column(children: list.toList())) - : ListView.builder(itemCount: list.length, itemBuilder: (_, index) => list.elementAt(index)); - } - - ///搜索 - void search(SearchModel? val) { - setState(() { - searchModel = val; - }); - } - - ///搜索过滤 - LinkedHashMap searchFilter(SearchModel searchModel) { - LinkedHashMap result = LinkedHashMap(); - - containerMap.forEach((key, domainRequests) { - var body = domainRequests.search(searchModel); - if (body.isNotEmpty) { - result[key] = domainRequests.copy(body: body, selected: searchView[key]?.currentSelected); - } - }); - - return result; - } - - ///高亮处理 - highlightHandler() { - //获取所有请求Widget - List requests = containerMap.values.map((e) => e.body).expand((element) => element).toList(); - for (RequestWidget request in requests) { - GlobalKey key = request.key as GlobalKey; - key.currentState?.setState(() {}); - } - } - - ///添加请求 - add(Channel channel, HttpRequest request) { - String? host = request.remoteDomain(); - if (host == null) { - return; - } - - //按照域名分类 - DomainRequests domainRequests = getDomainRequests(request); - var isNew = domainRequests.body.isEmpty; - - domainRequests.addRequest(request.requestId, request); - //搜索视图 - if (searchModel?.isNotEmpty == true && searchModel?.filter(request, null) == true) { - searchView[host]?.addRequest(request.requestId, request); - } - - if (isNew) { - setState(() { - containerMap[host] = domainRequests; - }); - } - } - - DomainRequests getDomainRequests(HttpRequest request) { - var host = request.remoteDomain()!; - DomainRequests? domainRequests = containerMap[host]; - if (domainRequests == null) { - domainRequests = DomainRequests( - host, - proxyServer: widget.panel.proxyServer, - trailing: appIcon(request), - onDelete: deleteHost, - onRequestRemove: (req) { - widget.onRemove?.call([req]); - changeState(); - }, - ); - containerMap[host] = domainRequests; - } - - return domainRequests; - } - - Widget? appIcon(HttpRequest request) { - var processInfo = request.processInfo; - if (processInfo == null) { - return null; - } - - return futureWidget( - processInfo.getIcon(), - (data) => - data.isEmpty ? const SizedBox() : Image.memory(data, width: 23, height: Platform.isWindows ? 16 : null)); - } - - ///移除域名 - deleteHost(String host) { - DomainRequests? domainRequests = containerMap.remove(host); - if (domainRequests == null) { - return; - } - setState(() {}); - - widget.onRemove?.call(domainRequests.body.map((e) => e.request).toList()); - } - - ///添加响应 - addResponse(ChannelContext channelContext, HttpResponse response) { - String domain = channelContext.host!.domain; - DomainRequests? domainRequests = containerMap[domain]; - var pathRow = domainRequests?.getRequest(response); - pathRow?.setResponse(response); - if (pathRow == null) { - return; - } - - //搜索视图 - if (searchModel?.isNotEmpty == true && searchModel?.filter(pathRow.request, response) == true) { - var requests = searchView[domain]; - if (requests?.getRequest(response) == null) { - requests?.addRequest(response.requestId, pathRow.request); - } - requests?.getRequest(response)?.setResponse(response); - } - } - - remove(List list) { - for (var request in list) { - String? host = request.remoteDomain(); - containerMap[host]?._removeRequest(request); - } - } - - ///清理 - clean() { - setState(() { - containerMap.clear(); - searchView.clear(); - - var container = widget.list; - for (var request in container.source) { - DomainRequests domainRequests = getDomainRequests(request); - domainRequests.addRequest(request.requestId, request); - } - }); - } - - List currentView() { - var container = containerMap.values; - if (searchModel?.isNotEmpty == true) { - container = searchView.values; - } - return container.expand((list) => list.body.map((it) => it.request)).toList(); - } -} - -///标题和内容布局 标题是域名 内容是域名下请求 -class DomainRequests extends StatefulWidget { - //请求ID和请求的映射 - final Map requestMap = HashMap(); - - final String domain; - final ProxyServer proxyServer; - final Widget? trailing; - - //请求列表 - final Queue body = Queue(); - - //是否选中 - final bool selected; - - //移除回调 - final Function(String host)? onDelete; - final Function(HttpRequest request)? onRequestRemove; - - DomainRequests(this.domain, - {this.selected = false, this.onDelete, required this.proxyServer, this.onRequestRemove, this.trailing}) - : super(key: GlobalKey<_DomainRequestsState>()); - - ///添加请求 - void addRequest(String? requestId, HttpRequest request) { - if (requestMap.containsKey(requestId)) return; - - var requestWidget = RequestWidget(request, - index: body.length, proxyServer: proxyServer, displayDomain: false, remove: (it) => _remove(it)); - body.addFirst(requestWidget); - if (requestId == null) { - return; - } - requestMap[requestId] = requestWidget; - changeState(); - } - - RequestWidget? getRequest(HttpResponse response) { - return requestMap[response.request?.requestId ?? response.requestId]; - } - - setTrailing(Widget? trailing) { - var state = key as GlobalKey<_DomainRequestsState>; - state.currentState?.trailing = trailing; - } - - _remove(RequestWidget requestWidget) { - if (body.remove(requestWidget)) { - onRequestRemove?.call(requestWidget.request); - changeState(); - } - } - - _removeRequest(HttpRequest request) { - var requestWidget = requestMap.remove(request.requestId); - if (requestWidget != null) { - _remove(requestWidget); - } - } - - ///根据文本过滤 - Iterable search(SearchModel searchModel) { - return body - .where((element) => searchModel.filter(element.request, element.response.get() ?? element.request.response)); - } - - ///复制 - DomainRequests copy({Iterable? body, bool? selected}) { - var state = key as GlobalKey<_DomainRequestsState>; - var headerBody = DomainRequests(domain, - trailing: trailing, - selected: selected ?? state.currentState?.selected == true, - onDelete: onDelete, - onRequestRemove: onRequestRemove, - proxyServer: proxyServer); - if (body != null) { - headerBody.body.addAll(body); - } - return headerBody; - } - - bool get currentSelected { - var state = key as GlobalKey<_DomainRequestsState>; - return state.currentState?.selected == true; - } - - changeState() { - var state = key as GlobalKey<_DomainRequestsState>; - state.currentState?.changeState(); - } - - @override - State createState() { - return _DomainRequestsState(); - } -} - -class _DomainRequestsState extends State { - final GlobalKey transitionState = GlobalKey(); - late Configuration configuration; - late bool selected; - Widget? trailing; - bool changing = false; - - AppLocalizations get localizations => AppLocalizations.of(context)!; - - @override - void initState() { - super.initState(); - configuration = widget.proxyServer.configuration; - selected = widget.selected; - trailing = widget.trailing; - } - - changeState() { - //防止频繁刷新 - if (!changing) { - changing = true; - Future.delayed(const Duration(milliseconds: 500), () { - setState(() { - changing = false; - }); - transitionState.currentState?.show(); - }); - } - } - - @override - Widget build(BuildContext context) { - return Column(children: [ - _hostWidget(widget.domain), - Offstage(offstage: !selected, child: Column(children: widget.body.toList())) - ]); - } - - //domain title - Widget _hostWidget(String title) { - var host = GestureDetector( - onSecondaryTap: menu, - child: ListTile( - minLeadingWidth: 25, - leading: Icon(selected ? Icons.arrow_drop_down : Icons.arrow_right, size: 18), - trailing: trailing, - dense: true, - horizontalTitleGap: 0, - contentPadding: const EdgeInsets.only(left: 3, right: 8), - visualDensity: const VisualDensity(vertical: -3.6), - title: Text(title, - textAlign: TextAlign.left, - style: const TextStyle(fontSize: 14), - maxLines: 1, - overflow: TextOverflow.ellipsis), - onTap: () { - setState(() { - selected = !selected; - }); - })); - - return ColorTransition( - key: transitionState, - duration: const Duration(milliseconds: 1800), - begin: Theme.of(context).focusColor, - startAnimation: false, - child: host); - } - - //域名右键菜单 - menu() { - Menu menu = Menu(items: [ - MenuItem( - label: localizations.copyHost, - onClick: (_) { - Clipboard.setData(ClipboardData(text: Uri.parse(widget.domain).host)); - FlutterToastr.show(localizations.copied, context); - }), - MenuItem.separator(), - MenuItem( - label: localizations.domainFilter, - type: 'submenu', - submenu: hostFilterMenu(), - ), - MenuItem.separator(), - MenuItem(label: localizations.repeatDomainRequests, onClick: (_) => repeatDomainRequests()), - MenuItem.separator(), - MenuItem(label: localizations.delete, onClick: (_) => _delete()), - ]); - - popUpContextMenu(menu); - } - - //重复域名下请求 - void repeatDomainRequests() async { - var list = widget.body.toList().reversed; - for (var requestWidget in list) { - var request = requestWidget.request.copy(uri: requestWidget.request.requestUrl); - var proxyInfo = widget.proxyServer.isRunning ? ProxyInfo.of("127.0.0.1", widget.proxyServer.port) : null; - try { - await HttpClients.proxyRequest(request, proxyInfo: proxyInfo, timeout: const Duration(seconds: 3)); - if (mounted) FlutterToastr.show(localizations.reSendRequest, rootNavigator: true, context); - } catch (e) { - if (mounted) FlutterToastr.show('${localizations.fail}$e', rootNavigator: true, context); - } - } - } - - Menu hostFilterMenu() { - return Menu(items: [ - MenuItem( - label: localizations.domainBlacklist, - onClick: (_) { - HostFilter.blacklist.add(Uri.parse(widget.domain).host); - configuration.flushConfig(); - FlutterToastr.show(localizations.addSuccess, context); - }), - MenuItem( - label: localizations.domainWhitelist, - onClick: (_) { - HostFilter.whitelist.add(Uri.parse(widget.domain).host); - configuration.flushConfig(); - FlutterToastr.show(localizations.addSuccess, context); - }), - MenuItem( - label: localizations.deleteWhitelist, - onClick: (_) { - HostFilter.whitelist.remove(Uri.parse(widget.domain).host); - configuration.flushConfig(); - FlutterToastr.show(localizations.deleteSuccess, context); - }), - ]); - } - - _delete() { - widget.onDelete?.call(widget.domain); - widget.requestMap.clear(); - widget.body.clear(); - FlutterToastr.show(localizations.deleteSuccess, context); - } -} diff --git a/lib/ui/desktop/request/request.dart b/lib/ui/desktop/request/request.dart index 7455d15..dc1822a 100644 --- a/lib/ui/desktop/request/request.dart +++ b/lib/ui/desktop/request/request.dart @@ -29,12 +29,12 @@ import 'package:proxypin/network/host_port.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http_client.dart'; import 'package:proxypin/storage/favorites.dart'; -import 'package:proxypin/ui/component/state_component.dart'; import 'package:proxypin/ui/component/utils.dart'; import 'package:proxypin/ui/component/widgets.dart'; import 'package:proxypin/ui/content/panel.dart'; import 'package:proxypin/ui/desktop/request/repeat.dart'; import 'package:proxypin/ui/desktop/toolbar/setting/script.dart'; +import 'package:proxypin/ui/desktop/widgets/highlight.dart'; import 'package:proxypin/utils/curl.dart'; import 'package:proxypin/utils/lang.dart'; import 'package:proxypin/utils/python.dart'; @@ -80,6 +80,11 @@ class _RequestWidgetState extends State { AppLocalizations get localizations => AppLocalizations.of(context)!; + @override + void initState() { + super.initState(); + } + @override Widget build(BuildContext context) { var request = widget.request; @@ -126,7 +131,7 @@ class _RequestWidgetState extends State { return highlightColor; } - return KeywordHighlightDialog.getHighlightColor(path); + return DesktopKeywordHighlight.getHighlightColor(path); } void changeState() { @@ -271,7 +276,7 @@ class _RequestWidgetState extends State { MenuItem( label: localizations.keyword, onClick: (_) { - showDialog(context: context, builder: (BuildContext context) => const KeywordHighlightDialog()); + showDialog(context: context, builder: (BuildContext context) => const DesktopKeywordHighlight()); }), ], ); @@ -339,105 +344,3 @@ class _RequestWidgetState extends State { NetworkTabController.current?.change(widget.request, widget.response.get() ?? widget.request.response); } } - -//配置关键词高亮 -class KeywordHighlightDialog extends StatefulWidget { - static Map keywords = {}; - static ValueNotifier keywordsController = ValueNotifier(keywords); - - static Color? getHighlightColor(String key) { - for (var entry in keywords.entries) { - if (key.contains(entry.value)) { - return entry.key; - } - } - return null; - } - - const KeywordHighlightDialog({super.key}); - - @override - State createState() => _KeywordHighlightState(); -} - -class _KeywordHighlightState extends State { - @override - Widget build(BuildContext context) { - AppLocalizations localizations = AppLocalizations.of(context)!; - var colors = { - Colors.red: localizations.red, - Colors.yellow.shade600: localizations.yellow, - Colors.blue: localizations.blue, - Colors.green: localizations.green, - Colors.grey: localizations.gray, - }; - - var map = Map.of(KeywordHighlightDialog.keywords); - - return AlertDialog( - title: ListTile( - title: Text(localizations.keyword + localizations.highlight, - textAlign: TextAlign.center, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500))), - titlePadding: const EdgeInsets.all(0), - actionsPadding: const EdgeInsets.only(right: 10, bottom: 10), - contentPadding: const EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 5), - actions: [ - TextButton( - child: Text(localizations.cancel), - onPressed: () => Navigator.of(context).pop(), - ), - TextButton( - child: Text(localizations.done), - onPressed: () { - KeywordHighlightDialog.keywords = map; - Navigator.of(context).pop(); - }, - ), - ], - content: SizedBox( - height: 180, - width: 400, - child: DefaultTabController( - length: colors.length, - child: Scaffold( - appBar: TabBar(tabs: colors.entries.map((e) => Tab(text: e.value)).toList()), - body: TabBarView( - children: colors.entries - .map((e) => KeepAliveWrapper( - child: Padding( - padding: const EdgeInsets.all(15), - child: TextFormField( - minLines: 2, - maxLines: 2, - initialValue: map[e.key], - onChanged: (value) { - if (value.isEmpty) { - map.remove(e.key); - } else { - map[e.key] = value; - } - }, - decoration: decoration(localizations.keyword), - )))) - .toList()), - ), - ), - ), - ); - } - - InputDecoration decoration(String label, {String? hintText}) { - return InputDecoration( - floatingLabelBehavior: FloatingLabelBehavior.always, - labelText: label, - isDense: true, - border: const OutlineInputBorder(), - ); - } - - @override - void dispose() { - KeywordHighlightDialog.keywordsController.value = Map.from(KeywordHighlightDialog.keywords); - super.dispose(); - } -} diff --git a/lib/ui/desktop/request/request_sequence.dart b/lib/ui/desktop/request/request_sequence.dart index b32abbd..48e6cc3 100644 --- a/lib/ui/desktop/request/request_sequence.dart +++ b/lib/ui/desktop/request/request_sequence.dart @@ -23,6 +23,7 @@ import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/ui/component/utils.dart'; import 'package:proxypin/ui/desktop/request/model/search_model.dart'; import 'package:proxypin/ui/desktop/request/request.dart'; +import 'package:proxypin/ui/desktop/widgets/highlight.dart'; import 'package:proxypin/utils/listenable_list.dart'; ///请求序列 列表 @@ -67,7 +68,7 @@ class RequestSequenceState extends State with AutomaticKeepAliv highlightHandler(); }); }; - KeywordHighlightDialog.keywordsController.addListener(highlightListener); + DesktopKeywordHighlight.keywordsController.addListener(highlightListener); } changeState() { @@ -87,7 +88,7 @@ class RequestSequenceState extends State with AutomaticKeepAliv @override void dispose() { - KeywordHighlightDialog.keywordsController.removeListener(highlightListener); + DesktopKeywordHighlight.keywordsController.removeListener(highlightListener); super.dispose(); } @@ -95,11 +96,12 @@ class RequestSequenceState extends State with AutomaticKeepAliv Widget build(BuildContext context) { super.build(context); return ListView.separated( - cacheExtent: 1500, + cacheExtent: 1000, separatorBuilder: (context, index) => Divider(thickness: 0.2, height: 0, color: Theme.of(context).dividerColor), itemCount: view.length, itemBuilder: (context, index) { return RequestWidget( + key: ValueKey(view.elementAt(index).requestId), view.elementAt(index), index: view.length - index, trailing: appIcon(view.elementAt(index)), diff --git a/lib/ui/desktop/widgets/highlight.dart b/lib/ui/desktop/widgets/highlight.dart new file mode 100644 index 0000000..86c27fa --- /dev/null +++ b/lib/ui/desktop/widgets/highlight.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:proxypin/ui/component/state_component.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +///配置关键词高亮 +///@Author: WangHongEn +class DesktopKeywordHighlight extends StatefulWidget { + static Map keywords = {}; + static ValueNotifier keywordsController = ValueNotifier(keywords); + + static Color? getHighlightColor(String key) { + for (var entry in keywords.entries) { + if (key.contains(entry.value)) { + return entry.key; + } + } + return null; + } + + const DesktopKeywordHighlight({super.key}); + + @override + State createState() => _KeywordHighlightState(); +} + +class _KeywordHighlightState extends State { + @override + Widget build(BuildContext context) { + AppLocalizations localizations = AppLocalizations.of(context)!; + var colors = { + Colors.red: localizations.red, + Colors.yellow.shade600: localizations.yellow, + Colors.blue: localizations.blue, + Colors.green: localizations.green, + Colors.grey: localizations.gray, + }; + + var map = Map.of(DesktopKeywordHighlight.keywords); + + return AlertDialog( + title: ListTile( + title: Text(localizations.keyword + localizations.highlight, + textAlign: TextAlign.center, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500))), + titlePadding: const EdgeInsets.all(0), + actionsPadding: const EdgeInsets.only(right: 10, bottom: 10), + contentPadding: const EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 5), + actions: [ + TextButton( + child: Text(localizations.cancel), + onPressed: () => Navigator.of(context).pop(), + ), + TextButton( + child: Text(localizations.done), + onPressed: () { + DesktopKeywordHighlight.keywords = map; + Navigator.of(context).pop(); + }, + ), + ], + content: SizedBox( + height: 180, + width: 400, + child: DefaultTabController( + length: colors.length, + child: Scaffold( + appBar: TabBar(tabs: colors.entries.map((e) => Tab(text: e.value)).toList()), + body: TabBarView( + children: colors.entries + .map((e) => KeepAliveWrapper( + child: Padding( + padding: const EdgeInsets.all(15), + child: TextFormField( + minLines: 2, + maxLines: 2, + initialValue: map[e.key], + onChanged: (value) { + if (value.isEmpty) { + map.remove(e.key); + } else { + map[e.key] = value; + } + }, + decoration: decoration(localizations.keyword), + )))) + .toList()), + ), + ), + ), + ); + } + + InputDecoration decoration(String label, {String? hintText}) { + return InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.always, + labelText: label, + isDense: true, + border: const OutlineInputBorder(), + ); + } + + @override + void dispose() { + DesktopKeywordHighlight.keywordsController.value = Map.from(DesktopKeywordHighlight.keywords); + super.dispose(); + } +} diff --git a/lib/ui/mobile/menu/drawer.dart b/lib/ui/mobile/menu/drawer.dart index 92274a7..71880cb 100644 --- a/lib/ui/mobile/menu/drawer.dart +++ b/lib/ui/mobile/menu/drawer.dart @@ -23,7 +23,7 @@ import 'package:proxypin/network/components/manager/request_block_manager.dart'; import 'package:proxypin/network/components/manager/request_rewrite_manager.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/storage/histories.dart'; -import 'package:proxypin/ui/component/toolbox.dart'; +import 'package:proxypin/ui/component/toolbox/toolbox.dart'; import 'package:proxypin/ui/component/utils.dart'; import 'package:proxypin/ui/configuration.dart'; import 'package:proxypin/ui/mobile/setting/preference.dart'; diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index 33ca71f..3af6b16 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -32,7 +32,7 @@ import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/websocket.dart'; import 'package:proxypin/network/http_client.dart'; import 'package:proxypin/ui/component/memory_cleanup.dart'; -import 'package:proxypin/ui/component/toolbox.dart'; +import 'package:proxypin/ui/component/toolbox/toolbox.dart'; import 'package:proxypin/ui/configuration.dart'; import 'package:proxypin/ui/content/panel.dart'; import 'package:proxypin/ui/launch/launch.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 0923651..d58924a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: proxypin description: ProxyPin publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.1.5+15 +version: 1.1.6+15 environment: sdk: '>=3.0.2 <4.0.0' @@ -22,7 +22,7 @@ dependencies: git: url: https://gitee.com/wanghongenpin/flutter-plugins.git path: packages/desktop_multi_window - path_provider: ^2.1.4 + path_provider: ^2.1.5 file_picker: ^8.1.3 proxy_manager: ^0.0.3 permission_handler: ^11.3.1