mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-03-15 04:23:17 +08:00
Desktop toolbox timestamp
This commit is contained in:
@@ -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.**
|
||||
|
||||
@@ -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优化。**
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -303,5 +303,10 @@
|
||||
"saveImage": "保存图片",
|
||||
"selectImage": "选择图片",
|
||||
"inputContent": "输入内容",
|
||||
"errorCorrectLevel": "纠错等级"
|
||||
"errorCorrectLevel": "纠错等级",
|
||||
"output": "输出",
|
||||
"timestamp": "时间戳",
|
||||
"convert": "转换",
|
||||
"time": "时间",
|
||||
"nowTimestamp": "当前时间戳(秒)"
|
||||
}
|
||||
@@ -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('=');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
@@ -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(
|
||||
164
lib/ui/component/toolbox/timestamp.dart
Normal file
164
lib/ui/component/toolbox/timestamp.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()) {
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
543
lib/ui/desktop/request/domians.dart
Normal file
543
lib/ui/desktop/request/domians.dart
Normal 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)));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
106
lib/ui/desktop/widgets/highlight.dart
Normal file
106
lib/ui/desktop/widgets/highlight.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user