桌面端增加历史记录功能

This commit is contained in:
wanghongen
2023-09-18 01:41:43 +08:00
parent 36e32d298c
commit 69152ad974
26 changed files with 847 additions and 154 deletions

View File

@@ -4,10 +4,12 @@ import 'package:network_proxy/network/bin/server.dart';
import 'package:network_proxy/network/channel.dart';
import 'package:network_proxy/network/handler.dart';
import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/ui/component/state_component.dart';
import 'package:network_proxy/ui/component/toolbox.dart';
import 'package:network_proxy/ui/content/panel.dart';
import 'package:network_proxy/ui/desktop/left/domain.dart';
import 'package:network_proxy/ui/desktop/left/favorite.dart';
import 'package:network_proxy/ui/component/toolbox.dart';
import 'package:network_proxy/ui/desktop/left/history.dart';
import 'package:network_proxy/ui/desktop/toolbar/toolbar.dart';
import '../component/split_view.dart';
@@ -26,13 +28,13 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
final PageController pageController = PageController();
final ValueNotifier<int> _selectIndex = ValueNotifier(0);
late ProxyServer proxyServer;
late ProxyServer proxyServer = ProxyServer(widget.configuration);
late NetworkTabController panel;
final List<NavigationRailDestination> destinations = const [
NavigationRailDestination(icon: Icon(Icons.workspaces), label: Text("抓包", style: TextStyle(fontSize: 12))),
// NavigationRailDestination(icon: Icon(Icons.history), label: Text("历史", style: TextStyle(fontSize: 12))),
NavigationRailDestination(icon: Icon(Icons.favorite), label: Text("收藏", style: TextStyle(fontSize: 12))),
NavigationRailDestination(icon: Icon(Icons.history), label: Text("历史", style: TextStyle(fontSize: 12))),
NavigationRailDestination(icon: Icon(Icons.construction), label: Text("工具箱", style: TextStyle(fontSize: 12))),
];
@@ -49,7 +51,7 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
@override
void initState() {
super.initState();
proxyServer = ProxyServer(widget.configuration, listener: this);
proxyServer.addListener(this);
panel = NetworkTabController(tabStyle: const TextStyle(fontSize: 16), proxyServer: proxyServer);
if (widget.configuration.upgradeNoticeV2) {
@@ -62,7 +64,6 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
@override
Widget build(BuildContext context) {
final domainWidget = DomainWidget(key: domainStateKey, proxyServer: proxyServer, panel: panel);
return Scaffold(
appBar: Tab(child: Toolbar(proxyServer, domainStateKey, sideNotifier: _selectIndex)),
body: Row(
@@ -86,8 +87,14 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
ratio: 0.3,
minRatio: 0.15,
maxRatio: 0.9,
left: PageView(
controller: pageController, children: [domainWidget, Favorites(panel: panel), const Toolbox()]),
left: PageView(controller: pageController, physics: const NeverScrollableScrollPhysics(), children: [
domainWidget,
Favorites(panel: panel),
KeepAliveWrapper(
child: HistoryPageWidget(
proxyServer: proxyServer, domainWidgetState: domainStateKey, panel: panel)),
const Toolbox()
]),
right: panel),
)
],
@@ -130,12 +137,11 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
'提示默认不会开启HTTPS抓包请安装证书后再开启HTTPS抓包。\n'
'点击的HTTPS抓包(加锁图标),选择安装根证书,按照提示操作即可。\n\n'
'新增更新:\n'
'1. 增加工具箱提供常用编解码增加HTTP请求,可粘贴cURL格式解析发起请求\n'
'2. 请求编辑发送可直接查看响应体,发送请求无需开启代理\n'
'3. 详情增加快速请求重写, 复制cURL格式可直接导入Postman\n'
'4. 增加请求收藏功能\n'
'5. 主题增加Material3切换\n'
'6. 请求删除&大响应体直接转发;',
'1. 增加历史记录功能\n'
'2. 请求重写增加名称&URL参数重写\n'
'3. 请求重写增加重定向\n'
'4. 建立连接异常显示请求体\n'
'5. 请求编辑重发响应体查看增加多种格式详情Body体增加快速解码入口',
style: TextStyle(fontSize: 14)));
});
}

View File

@@ -1,6 +1,5 @@
import 'dart:collection';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:network_proxy/network/bin/configuration.dart';
import 'package:network_proxy/network/bin/server.dart';
@@ -10,17 +9,21 @@ import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/network/util/attribute_keys.dart';
import 'package:network_proxy/network/util/host_filter.dart';
import 'package:network_proxy/ui/component/transition.dart';
import 'package:network_proxy/ui/component/utils.dart';
import 'package:network_proxy/ui/component/widgets.dart';
import 'package:network_proxy/ui/content/panel.dart';
import 'package:network_proxy/ui/desktop/left/model/search_model.dart';
import 'package:network_proxy/ui/desktop/left/path.dart';
import 'package:network_proxy/ui/content/panel.dart';
import 'package:network_proxy/ui/desktop/left/search.dart';
///左侧域名
class DomainWidget extends StatefulWidget {
final NetworkTabController panel;
final ProxyServer proxyServer;
final List<HttpRequest>? list;
final bool shrinkWrap;
const DomainWidget({super.key, required this.panel, required this.proxyServer});
const DomainWidget({super.key, required this.panel, required this.proxyServer, this.list, this.shrinkWrap = true});
@override
State<StatefulWidget> createState() {
@@ -28,7 +31,8 @@ class DomainWidget extends StatefulWidget {
}
}
class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClientMixin{
class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClientMixin {
List<HttpRequest> container = [];
LinkedHashMap<HostAndPort, HeaderBody> containerMap = LinkedHashMap<HostAndPort, HeaderBody>();
LinkedHashMap<HostAndPort, HeaderBody> searchView = LinkedHashMap<HostAndPort, HeaderBody>();
@@ -47,6 +51,22 @@ class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClien
}
}
@override
void initState() {
super.initState();
widget.list?.forEach((request) {
var host = HostAndPort.of(request.requestUrl);
HeaderBody? headerBody = containerMap[host];
if (headerBody == null) {
headerBody = HeaderBody(host, proxyServer: widget.panel.proxyServer, onRemove: () => remove(host));
containerMap[host] = headerBody;
}
var listURI =
PathRow(request, widget.panel, proxyServer: widget.panel.proxyServer, remove: (it) => headerBody!.remove(it));
headerBody.addBody(null, listURI);
});
}
@override
bool get wantKeepAlive => true;
@@ -63,8 +83,11 @@ class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClien
searchView.clear();
}
Widget body = widget.shrinkWrap
? SingleChildScrollView(child: Column(children: list.toList()))
: ListView.builder(itemCount: list.length, cacheExtent: 1000, itemBuilder: (_, index) => list.elementAt(index));
return Scaffold(
body: SingleChildScrollView(child: Column(children: list.toList())),
body: body,
bottomNavigationBar: Search(onSearch: (val) {
setState(() {
searchModel = val;
@@ -88,11 +111,13 @@ class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClien
///添加请求
add(Channel channel, HttpRequest request) {
container.add(request);
HostAndPort hostAndPort = channel.getAttribute(AttributeKeys.host);
//按照域名分类
HeaderBody? headerBody = containerMap[hostAndPort];
if (headerBody != null) {
var listURI = PathRow(request, widget.panel, proxyServer: widget.proxyServer, remove: (it) => headerBody!.remove(it));
var listURI =
PathRow(request, widget.panel, proxyServer: widget.proxyServer, remove: (it) => headerBody!.remove(it));
headerBody.addBody(channel.id, listURI);
//搜索视图
@@ -103,7 +128,8 @@ class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClien
}
headerBody = HeaderBody(hostAndPort, proxyServer: widget.proxyServer, onRemove: () => remove(hostAndPort));
var listURI = PathRow(request, widget.panel, proxyServer: widget.proxyServer, remove: (it) => headerBody!.remove(it));
var listURI =
PathRow(request, widget.panel, proxyServer: widget.proxyServer, remove: (it) => headerBody!.remove(it));
headerBody.addBody(channel.id, listURI);
setState(() {
containerMap[hostAndPort] = headerBody!;
@@ -113,6 +139,7 @@ class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClien
remove(HostAndPort hostAndPort) {
setState(() {
containerMap.remove(hostAndPort);
container.removeWhere((element) => element.hostAndPort == hostAndPort);
});
}
@@ -140,6 +167,7 @@ class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClien
clean() {
widget.panel.change(null, null);
setState(() {
container.clear();
containerMap.clear();
});
}
@@ -156,7 +184,7 @@ class HeaderBody extends StatefulWidget {
final ProxyServer proxyServer;
//请求列表
final Queue<PathRow> _body = Queue();
final Queue<PathRow> body = Queue();
//是否选中
final bool selected;
@@ -168,8 +196,11 @@ class HeaderBody extends StatefulWidget {
: super(key: GlobalKey<_HeaderBodyState>());
///添加请求
void addBody(String key, PathRow widget) {
_body.addFirst(widget);
void addBody(String? key, PathRow widget) {
body.addFirst(widget);
if (key == null) {
return;
}
channelIdPathMap[key] = widget;
changeState();
}
@@ -179,14 +210,15 @@ class HeaderBody extends StatefulWidget {
}
remove(PathRow pathRow) {
if (_body.remove(pathRow)) {
if (body.remove(pathRow)) {
changeState();
}
}
///根据文本过滤
Iterable<PathRow> search(SearchModel searchModel) {
return _body.where((element) => searchModel.filter(element.request, element.response.get()));
return body
.where((element) => searchModel.filter(element.request, element.response.get() ?? element.request.response));
}
///复制
@@ -195,7 +227,7 @@ class HeaderBody extends StatefulWidget {
var headerBody = HeaderBody(header,
selected: selected ?? state.currentState?.selected == true, onRemove: onRemove, proxyServer: proxyServer);
if (body != null) {
headerBody._body.addAll(body);
headerBody.body.addAll(body);
}
return headerBody;
}
@@ -237,13 +269,13 @@ class _HeaderBodyState extends State<HeaderBody> {
Widget build(BuildContext context) {
return Column(children: [
_hostWidget(widget.header.domain),
Offstage(offstage: !selected, child: Column(children: widget._body.toList()))
Offstage(offstage: !selected, child: Column(children: widget.body.toList()))
]);
}
Widget _hostWidget(String title) {
var host = GestureDetector(
onSecondaryLongPressDown: menu,
onSecondaryTapDown: menu,
child: ListTile(
minLeadingWidth: 25,
leading: Icon(selected ? Icons.arrow_drop_down : Icons.arrow_right, size: 18),
@@ -270,46 +302,42 @@ class _HeaderBodyState extends State<HeaderBody> {
}
//域名右键菜单
menu(LongPressDownDetails details) {
showMenu(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
details.globalPosition.dy,
),
menu(TapDownDetails details) {
showContextMenu(
context,
details.globalPosition,
items: <PopupMenuEntry>[
PopupMenuItem(
height: 38,
child: const Text("添加黑名单", style: TextStyle(fontSize: 14)),
CustomPopupMenuItem(
height: 35,
child: const Text("添加黑名单", style: TextStyle(fontSize: 13)),
onTap: () {
HostFilter.blacklist.add(widget.header.host);
configuration.flushConfig();
}),
PopupMenuItem(
height: 38,
child: const Text("添加白名单", style: TextStyle(fontSize: 14)),
CustomPopupMenuItem(
height: 35,
child: const Text("添加白名单", style: TextStyle(fontSize: 13)),
onTap: () {
HostFilter.whitelist.add(widget.header.host);
configuration.flushConfig();
}),
PopupMenuItem(
height: 38,
child: const Text("删除白名单", style: TextStyle(fontSize: 14)),
CustomPopupMenuItem(
height: 35,
child: const Text("删除白名单", style: TextStyle(fontSize: 13)),
onTap: () {
HostFilter.whitelist.remove(widget.header.host);
configuration.flushConfig();
}),
PopupMenuItem(height: 38, child: const Text("删除", style: TextStyle(fontSize: 14)), onTap: () => _delete()),
const PopupMenuDivider(height: 0.3),
CustomPopupMenuItem(
height: 35, child: const Text("删除", style: TextStyle(fontSize: 13)), onTap: () => _delete()),
],
);
}
_delete() {
widget.channelIdPathMap.clear();
widget._body.clear();
widget.body.clear();
widget.onRemove?.call();
}
}

View File

@@ -12,6 +12,7 @@ import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/network/http_client.dart';
import 'package:network_proxy/storage/favorites.dart';
import 'package:network_proxy/ui/component/utils.dart';
import 'package:network_proxy/ui/component/widgets.dart';
import 'package:network_proxy/ui/content/panel.dart';
import 'package:network_proxy/utils/curl.dart';
import 'package:window_manager/window_manager.dart';
@@ -101,14 +102,9 @@ class _FavoriteItemState extends State<_FavoriteItem> {
///右键菜单
menu(LongPressDownDetails details) {
showMenu(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
details.globalPosition.dy,
),
showContextMenu(
context,
details.globalPosition,
items: <PopupMenuEntry>[
popupItem("复制请求链接", onTap: () {
var requestUrl = widget.request.requestUrl;
@@ -141,7 +137,7 @@ class _FavoriteItemState extends State<_FavoriteItem> {
}
PopupMenuItem popupItem(String text, {VoidCallback? onTap}) {
return PopupMenuItem(height: 38, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 14)));
return CustomPopupMenuItem(height: 35, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 13)));
}
///请求编辑

View File

@@ -0,0 +1,234 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:date_format/date_format.dart';
import 'package:flutter/material.dart';
import 'package:network_proxy/network/bin/server.dart';
import 'package:network_proxy/network/channel.dart';
import 'package:network_proxy/network/handler.dart';
import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/storage/histories.dart';
import 'package:network_proxy/ui/component/utils.dart';
import 'package:network_proxy/ui/component/widgets.dart';
import 'package:network_proxy/utils/har.dart';
import '../../content/panel.dart';
import 'domain.dart';
///历史记录
class HistoryPageWidget extends StatelessWidget {
final ProxyServer proxyServer;
final GlobalKey<DomainWidgetState> domainWidgetState;
final NetworkTabController panel;
const HistoryPageWidget({super.key, required this.proxyServer, required this.domainWidgetState, required this.panel});
@override
Widget build(BuildContext context) {
return Navigator(
onGenerateRoute: (settings) {
switch (settings.name) {
case "/domain":
return MaterialPageRoute(builder: (_) => domainWidget(settings.arguments as Map));
default:
return MaterialPageRoute(
builder: (_) => futureWidget(
HistoryStorage.instance,
(storage) => _HistoryWidget(storage,
container: domainWidgetState.currentState!.container, proxyServer: proxyServer),
));
}
},
);
}
Widget domainWidget(Map arguments) {
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(40),
child: AppBar(
leading: BackButton(style: ButtonStyle(iconSize: MaterialStateProperty.all(15))),
centerTitle: false,
title: Text(arguments['title'], style: const TextStyle(fontSize: 14)),
)),
body: futureWidget(HistoryStorage.instance.then((value) => value.getRequests(arguments['name'])), (data) {
return DomainWidget(panel: panel, proxyServer: proxyServer, list: data, shrinkWrap: false);
}));
}
}
class _HistoryWidget extends StatefulWidget {
// 存储
final HistoryStorage storage;
final List<HttpRequest> container;
final ProxyServer proxyServer;
const _HistoryWidget(this.storage, {required this.container, required this.proxyServer});
@override
State<StatefulWidget> createState() {
return _HistoryState();
}
}
class _HistoryState extends State<_HistoryWidget> implements EventListener {
///是否保存会话
static bool _sessionSaved = false;
static WriteTask? writeTask;
// 存储
late HistoryStorage storage;
late List<HttpRequest> container;
late ProxyServer proxyServer;
@override
void initState() {
super.initState();
storage = widget.storage;
container = widget.container;
proxyServer = widget.proxyServer;
}
@override
Widget build(BuildContext context) {
print("_HistoryState build");
List<Widget> children = [];
if (!_sessionSaved) {
//当前会话未保存,是否保存当前会话
children.add(buildSaveSession(container));
}
var entries = storage.histories.entries;
for (int i = entries.length - 1; i >= 0; i--) {
var entry = entries.elementAt(i);
children.add(buildItem(context, entry.key, entry.value));
}
return ListView.separated(
itemCount: children.length,
itemBuilder: (_, index) => children[index],
separatorBuilder: (_, index) => const Divider(thickness: 0.3, height: 0),
);
}
//构建保存会话
Widget buildSaveSession(List<HttpRequest> container) {
var name = formatDate(DateTime.now(), [mm, '-', d, ' ', HH, ':', nn, ':', ss]);
return ListTile(
dense: true,
title: Text(name),
subtitle: Text("当前会话未保存 记录数 ${container.length}"),
trailing: TextButton.icon(
icon: const Icon(Icons.save),
label: const Text("保存"),
onPressed: () async {
await _writeHarFile(container, name);
setState(() {
_sessionSaved = true;
});
},
),
onTap: () => ContextMenuController.removeAny());
}
//构建历史记录
Widget buildItem(BuildContext context, String name, HistoryItem item) {
return GestureDetector(
onSecondaryTapDown: (details) => {
showContextMenu(context, details.globalPosition, items: [
CustomPopupMenuItem(
height: 35,
child: const Text('删除', style: TextStyle(fontSize: 13)),
onTap: () {
setState(() {
if (name == writeTask?.name) {
writeTask?.timer?.cancel();
writeTask?.open.close();
}
storage.removeHistory(name);
});
})
])
},
child: ListTile(
dense: true,
title: Text(name),
subtitle: Text("记录数 ${item.requestLength} 文件 ${item.size}"),
onTap: () {
ContextMenuController.removeAny();
Navigator.pushNamed(context, '/domain',
arguments: {'title': '$name 记录数 ${item.requestLength}', 'name': name})
.then((value) => Future.delayed(const Duration(seconds: 60), () => storage.removeCache(name)));
}));
}
//写入文件
_writeHarFile(List<HttpRequest> container, String name) async {
var file = await HistoryStorage.openFile("${DateTime.now().millisecondsSinceEpoch}.txt");
print(file);
RandomAccessFile open = await file.open(mode: FileMode.append);
storage.addHistory(name, file, 0);
writeTask = WriteTask(name, open, storage, callback: () => setState(() {}));
writeTask?.writeList.addAll(container);
writeTask?.startTask();
proxyServer.addListener(this);
}
@override
void onRequest(Channel channel, HttpRequest request) {}
@override
void onResponse(Channel channel, HttpResponse response) async {
if (response.request == null) {
return;
}
writeTask?.writeList.add(response.request!);
}
}
class WriteTask {
final HistoryStorage historyStorage;
final RandomAccessFile open;
Queue writeList = Queue();
Timer? timer;
final Function? callback;
final String name;
WriteTask(this.name, this.open, this.historyStorage, {this.callback});
//写入任务
startTask() {
timer = Timer.periodic(const Duration(seconds: 15), (it) => writeTask());
}
//写入任务
writeTask() async {
if (writeList.isEmpty) {
return;
}
var history = historyStorage.getHistory(name);
int length = history.requestLength;
while (writeList.isNotEmpty) {
var request = writeList.removeFirst();
var har = Har.toHar(request);
await open.writeString(jsonEncode(har));
await open.writeString(",\n");
length++;
}
await open.flush(); //刷新
history.requestLength = length;
history.fileSize = await open.length();
historyStorage.updateHistory(name, history);
callback?.call();
}
}

View File

@@ -12,6 +12,7 @@ import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/network/http_client.dart';
import 'package:network_proxy/storage/favorites.dart';
import 'package:network_proxy/ui/component/utils.dart';
import 'package:network_proxy/ui/component/widgets.dart';
import 'package:network_proxy/ui/content/panel.dart';
import 'package:network_proxy/utils/curl.dart';
import 'package:network_proxy/utils/lang.dart';
@@ -55,11 +56,12 @@ class _PathRowState extends State<PathRow> {
title = '${request.method.name} ${Uri.parse(request.uri).path}';
} catch (_) {}
var time = formatDate(request.requestTime, [HH, ':', nn, ':', ss]);
return GestureDetector(
onSecondaryLongPressDown: menu,
onSecondaryTapDown: menu,
child: ListTile(
minLeadingWidth: 25,
leading: getIcon(widget.response.get()),
leading: getIcon(widget.response.get() ?? widget.request.response),
title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 1),
subtitle: Text(
'$time - [${response?.status.code ?? ''}] ${response?.contentType.name.toUpperCase() ?? ''} ${response?.costTime() ?? ''} ',
@@ -75,15 +77,10 @@ class _PathRowState extends State<PathRow> {
}
///右键菜单
menu(LongPressDownDetails details) {
showMenu(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
details.globalPosition.dy,
),
menu(TapDownDetails details) {
showContextMenu(
context,
details.globalPosition,
items: <PopupMenuEntry>[
popupItem("复制请求链接", onTap: () {
var requestUrl = widget.request.requestUrl;
@@ -97,6 +94,7 @@ class _PathRowState extends State<PathRow> {
Clipboard.setData(ClipboardData(text: curlRequest(widget.request)))
.then((value) => FlutterToastr.show('已复制到剪切板', context));
}),
const PopupMenuDivider(height: 0.3),
popupItem("重放请求", onTap: () {
var request = widget.request.copy(uri: widget.request.requestUrl);
HttpClients.proxyRequest(request);
@@ -112,6 +110,7 @@ class _PathRowState extends State<PathRow> {
FavoriteStorage.addFavorite(widget.request);
FlutterToastr.show('收藏成功', context);
}),
const PopupMenuDivider(height: 0.3),
popupItem("删除", onTap: () {
widget.remove?.call(widget);
}),
@@ -120,7 +119,7 @@ class _PathRowState extends State<PathRow> {
}
PopupMenuItem popupItem(String text, {VoidCallback? onTap}) {
return PopupMenuItem(height: 38, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 14)));
return CustomPopupMenuItem(height: 32, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 13)));
}
///请求编辑
@@ -157,6 +156,6 @@ class _PathRowState extends State<PathRow> {
});
}
selectedState = this;
widget.panel.change(widget.request, widget.response.get());
widget.panel.change(widget.request, widget.response.get() ?? widget.request.response);
}
}

View File

@@ -147,7 +147,7 @@ class _RuleAddDialogState extends State<RuleAddDialog> {
title: const Text("添加请求重写规则", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
scrollable: true,
content: Container(
constraints: const BoxConstraints(minWidth: 320),
constraints: const BoxConstraints(minWidth: 350, minHeight: 460),
child: Form(
key: formKey,
child: Column(
@@ -158,22 +158,25 @@ class _RuleAddDialogState extends State<RuleAddDialog> {
valueListenable: enableNotifier,
builder: (_, bool enable, __) {
return SwitchListTile(
dense: true,
contentPadding: const EdgeInsets.only(left: 0),
title: const Text('是否启用', textAlign: TextAlign.start),
value: enable,
onChanged: (value) => enableNotifier.value = value);
}),
TextFormField(
decoration: const InputDecoration(labelText: '名称'),
decoration: decoration('名称'),
initialValue: rule.name,
onSaved: (val) => rule.name = val,
),
const SizedBox(height: 5),
TextFormField(
decoration: const InputDecoration(labelText: '域名(可选)', hintText: 'baidu.com 不需要填写HTTP'),
decoration: decoration('域名(可选)', hintText: 'baidu.com 不需要填写HTTP'),
initialValue: rule.domain,
onSaved: (val) => rule.domain = val?.trim()),
const SizedBox(height: 5),
TextFormField(
decoration: const InputDecoration(labelText: 'Path', hintText: '/api/v1/*'),
decoration: decoration('Path', hintText: '/api/v1/*'),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Path不能为空';
@@ -182,9 +185,11 @@ class _RuleAddDialogState extends State<RuleAddDialog> {
},
initialValue: rule.path,
onSaved: (val) => rule.path = val!.trim()),
const SizedBox(height: 5),
DropdownButtonFormField<RuleType>(
decoration: const InputDecoration(labelText: '行为'),
value: rule.type,
isDense: true,
decoration: decoration('行为'),
items: RuleType.values
.map((e) =>
DropdownMenuItem(value: e, child: Text(e.name, style: const TextStyle(fontSize: 14))))
@@ -194,6 +199,7 @@ class _RuleAddDialogState extends State<RuleAddDialog> {
rule.type = val!;
});
}),
const SizedBox(height: 5),
...rewriteWidgets()
]))),
actions: [
@@ -222,11 +228,25 @@ class _RuleAddDialogState extends State<RuleAddDialog> {
]);
}
InputDecoration decoration(String label, {String? hintText}) {
Color color = Theme.of(context).colorScheme.primary;
// Color color = Colors.blueAccent;
return InputDecoration(
labelText: label,
hintText: hintText,
isDense: true,
border: UnderlineInputBorder(borderSide: BorderSide(width: 0.3, color: color)),
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(width: 0.3, color: color)),
focusedBorder: UnderlineInputBorder(borderSide: BorderSide(width: 1.5, color: color)));
}
List<Widget> rewriteWidgets() {
if (rule.type == RuleType.redirect) {
return [
TextFormField(
decoration: const InputDecoration(labelText: '重定向到:', hintText: 'http://www.example.com/api'),
decoration: decoration('重定向到:', hintText: 'http://www.example.com/api'),
maxLines: 3,
initialValue: rule.redirectUrl,
onSaved: (val) => rule.redirectUrl = val,
validator: (val) {
@@ -241,20 +261,22 @@ class _RuleAddDialogState extends State<RuleAddDialog> {
return [
TextFormField(
initialValue: rule.queryParam,
decoration: const InputDecoration(labelText: 'URL参数替换为:'),
decoration: decoration('URL参数替换为:'),
maxLines: 1,
onSaved: (val) => rule.queryParam = val),
const SizedBox(height: 5),
TextFormField(
initialValue: rule.requestBody,
decoration: const InputDecoration(labelText: '请求体替换为:'),
decoration: decoration('请求体替换为:'),
minLines: 1,
maxLines: 5,
onSaved: (val) => rule.requestBody = val),
const SizedBox(height: 5),
TextFormField(
initialValue: rule.responseBody,
minLines: 3,
maxLines: 15,
decoration: const InputDecoration(labelText: '响应体替换为:', hintText: '{"code":"200","data":{}}'),
maxLines: 10,
decoration: decoration('响应体替换为:', hintText: '{"code":"200","data":{}}'),
onSaved: (val) => rule.responseBody = val)
];
}

View File

@@ -46,15 +46,13 @@ class _SettingState extends State<Setting> {
itemBuilder: (context) {
return [
PopupMenuItem<String>(
padding: const EdgeInsets.all(0),
child: PortWidget(proxyServer: widget.proxyServer, textStyle: const TextStyle(fontSize: 13))),
PopupMenuItem<String>(
padding: const EdgeInsets.all(0),
child: ValueListenableBuilder(
valueListenable: enableDesktopListenable,
builder: (_, val, __) => setSystemProxy(),
)),
const PopupMenuItem(padding: EdgeInsets.all(0), child: ThemeSetting(dense: true)),
valueListenable: enableDesktopListenable,
builder: (_, val, __) => setSystemProxy(),
)),
const PopupMenuItem(child: ThemeSetting(dense: true)),
menuItem("域名过滤", onTap: hostFilter),
menuItem("请求重写", onTap: requestRewrite),
menuItem("外部代理设置", onTap: setExternalProxy),
@@ -71,7 +69,6 @@ class _SettingState extends State<Setting> {
PopupMenuItem<String> menuItem(String title, {GestureTapCallback? onTap}) {
return PopupMenuItem<String>(
padding: const EdgeInsets.all(0),
child: ListTile(
title: Text(title),
dense: true,
@@ -185,7 +182,6 @@ class _PortState extends State<PortWidget> {
@override
Widget build(BuildContext context) {
return Row(children: [
const Padding(padding: EdgeInsets.only(left: 16)),
Text("端口号:", style: widget.textStyle),
SizedBox(
width: 80,

View File

@@ -29,10 +29,8 @@ class _SslState extends State<SslWidget> {
itemBuilder: (context) {
return [
PopupMenuItem(
padding: const EdgeInsets.all(0),
child: _Switch(proxyServer: widget.proxyServer, onEnableChange: (val) => setState(() {}))),
PopupMenuItem(
padding: const EdgeInsets.all(0),
child: ListTile(
dense: true,
hoverColor: Colors.transparent,
@@ -44,7 +42,6 @@ class _SslState extends State<SslWidget> {
},
)),
PopupMenuItem<String>(
padding: const EdgeInsets.all(0),
child: ListTile(
title: const Text("安装根证书到 iOS"),
dense: true,
@@ -56,7 +53,6 @@ class _SslState extends State<SslWidget> {
}),
),
PopupMenuItem<String>(
padding: const EdgeInsets.all(0),
child: ListTile(
title: const Text("安装根证书到 Android"),
dense: true,
@@ -68,7 +64,6 @@ class _SslState extends State<SslWidget> {
}),
),
PopupMenuItem<String>(
padding: const EdgeInsets.all(0),
child: ListTile(
title: const Text("下载根证书"),
dense: true,
@@ -288,4 +283,4 @@ class _SwitchState extends State<_Switch> {
widget.proxyServer.configuration.flushConfig();
}
}
}
}