Desktop toolbox timestamp

This commit is contained in:
wanghongenpin
2024-11-08 19:05:56 +08:00
parent 06918b4621
commit 799008fe47
23 changed files with 885 additions and 621 deletions

View File

@@ -1,7 +1,7 @@
# ProxyPin
English | [中文](README_CN.md)
## Open source free packet capture toolSupport 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 Storehttps://apps.apple.com/app/proxypin/id6450932949
Android Google Playhttps://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.**

View File

@@ -22,6 +22,8 @@
iOS AppStore下载地址 https://apps.apple.com/app/proxypin/id6450932949
Android Google Playhttps://play.google.com/store/apps/details?id=com.network.proxy
TG: https://t.me/proxypin_tg
**接下来会持续完善功能和体验UI优化。**

View File

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

View File

@@ -303,5 +303,10 @@
"saveImage": "保存图片",
"selectImage": "选择图片",
"inputContent": "输入内容",
"errorCorrectLevel": "纠错等级"
"errorCorrectLevel": "纠错等级",
"output": "输出",
"timestamp": "时间戳",
"convert": "转换",
"time": "时间",
"nowTimestamp": "当前时间戳(秒)"
}

View File

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

View File

@@ -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<dynamic, dynamic> argument) {
if (argument['name'] == 'RegExpPage') {
return const RegExpPage();
}
if (argument['name'] == 'TimestampPage') {
return TimestampPage(windowId: windowId);
}
//脚本日志
if (argument['name'] == 'ScriptConsoleWidget') {
return ScriptConsoleWidget(windowId: windowId);

View File

@@ -98,7 +98,7 @@ class _CertHashPageState extends State<CertHashPage> {
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),

View File

@@ -145,7 +145,7 @@ class _JavaScriptState extends State<JavaScript> {
))))),
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(

View File

@@ -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<StatefulWidget> createState() {
return _TimestampPageState();
}
}
class _TimestampPageState extends State<TimestampPage> {
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>(EdgeInsets.symmetric(horizontal: 15, vertical: 8)),
// textStyle: WidgetStateProperty.all<TextStyle>(TextStyle(fontSize: 14)),
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
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);
}
}
}

View File

@@ -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<Toolbox> {
),
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()) {

View File

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

View File

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

View File

@@ -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<HttpRequest> list;
final bool shrinkWrap;
final Function(List<HttpRequest>)? onRemove;
const DomainList(
{super.key,
required this.proxyServer,
required this.list,
this.shrinkWrap = true,
required this.panel,
this.onRemove});
@override
State<StatefulWidget> createState() {
return DomainWidgetState();
}
}
class DomainWidgetState extends State<DomainList> with AutomaticKeepAliveClientMixin {
//域名和对应请求列表的映射
final LinkedHashMap<String, DomainRequests> containerMap = LinkedHashMap<String, DomainRequests>();
//搜索视图
LinkedHashMap<String, DomainRequests> searchView = LinkedHashMap<String, DomainRequests>();
//搜索的内容
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<String, DomainRequests> searchFilter(SearchModel searchModel) {
LinkedHashMap<String, DomainRequests> result = LinkedHashMap<String, DomainRequests>();
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<RequestWidget> requests = containerMap.values.map((e) => e.body).expand((element) => element).toList();
for (RequestWidget request in requests) {
GlobalKey key = request.key as GlobalKey<State>;
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<HttpRequest> 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<HttpRequest> 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<String, RequestWidget> requestMap = HashMap<String, RequestWidget>();
final String domain;
final ProxyServer proxyServer;
final Widget? trailing;
//请求列表
final Queue<RequestWidget> 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<RequestWidget> search(SearchModel searchModel) {
return body
.where((element) => searchModel.filter(element.request, element.response.get() ?? element.request.response));
}
///复制
DomainRequests copy({Iterable<RequestWidget>? 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<StatefulWidget> createState() {
return _DomainRequestsState();
}
}
class _DomainRequestsState extends State<DomainRequests> {
final GlobalKey<ColorTransitionState> transitionState = GlobalKey<ColorTransitionState>();
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)));
}
}

View File

@@ -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<DesktopRequestListWidget> 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<HttpRequest> list;
final bool shrinkWrap;
final Function(List<HttpRequest>)? onRemove;
const DomainList(
{super.key,
required this.proxyServer,
required this.list,
this.shrinkWrap = true,
required this.panel,
this.onRemove});
@override
State<StatefulWidget> createState() {
return DomainWidgetState();
}
}
class DomainWidgetState extends State<DomainList> with AutomaticKeepAliveClientMixin {
//域名和对应请求列表的映射
final LinkedHashMap<String, DomainRequests> containerMap = LinkedHashMap<String, DomainRequests>();
//搜索视图
LinkedHashMap<String, DomainRequests> searchView = LinkedHashMap<String, DomainRequests>();
//搜索的内容
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<String, DomainRequests> searchFilter(SearchModel searchModel) {
LinkedHashMap<String, DomainRequests> result = LinkedHashMap<String, DomainRequests>();
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<RequestWidget> requests = containerMap.values.map((e) => e.body).expand((element) => element).toList();
for (RequestWidget request in requests) {
GlobalKey key = request.key as GlobalKey<State>;
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<HttpRequest> 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<HttpRequest> 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<String, RequestWidget> requestMap = HashMap<String, RequestWidget>();
final String domain;
final ProxyServer proxyServer;
final Widget? trailing;
//请求列表
final Queue<RequestWidget> 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<RequestWidget> search(SearchModel searchModel) {
return body
.where((element) => searchModel.filter(element.request, element.response.get() ?? element.request.response));
}
///复制
DomainRequests copy({Iterable<RequestWidget>? 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<StatefulWidget> createState() {
return _DomainRequestsState();
}
}
class _DomainRequestsState extends State<DomainRequests> {
final GlobalKey<ColorTransitionState> transitionState = GlobalKey<ColorTransitionState>();
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);
}
}

View File

@@ -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<RequestWidget> {
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<RequestWidget> {
return highlightColor;
}
return KeywordHighlightDialog.getHighlightColor(path);
return DesktopKeywordHighlight.getHighlightColor(path);
}
void changeState() {
@@ -271,7 +276,7 @@ class _RequestWidgetState extends State<RequestWidget> {
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<RequestWidget> {
NetworkTabController.current?.change(widget.request, widget.response.get() ?? widget.request.response);
}
}
//配置关键词高亮
class KeywordHighlightDialog extends StatefulWidget {
static Map<Color, String> keywords = {};
static ValueNotifier keywordsController = ValueNotifier<Map>(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<KeywordHighlightDialog> createState() => _KeywordHighlightState();
}
class _KeywordHighlightState extends State<KeywordHighlightDialog> {
@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();
}
}

View File

@@ -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<RequestSequence> with AutomaticKeepAliv
highlightHandler();
});
};
KeywordHighlightDialog.keywordsController.addListener(highlightListener);
DesktopKeywordHighlight.keywordsController.addListener(highlightListener);
}
changeState() {
@@ -87,7 +88,7 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
@override
void dispose() {
KeywordHighlightDialog.keywordsController.removeListener(highlightListener);
DesktopKeywordHighlight.keywordsController.removeListener(highlightListener);
super.dispose();
}
@@ -95,11 +96,12 @@ class RequestSequenceState extends State<RequestSequence> 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)),

View File

@@ -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<Color, String> keywords = {};
static ValueNotifier keywordsController = ValueNotifier<Map>(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<DesktopKeywordHighlight> createState() => _KeywordHighlightState();
}
class _KeywordHighlightState extends State<DesktopKeywordHighlight> {
@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();
}
}

View File

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

View File

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

View File

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