diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 255d0a7..dfdd019 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -104,6 +104,9 @@ "enableSelect": "Enable Select", "disableSelect": "Disable Select", "deleteSelect": "Delete Select", + "testData": "Test Data", + "noChangesDetected": "No changes detected", + "enterMatchData": "Enter the data to be matched", "modifyRequestHeader": "Modify Header", "headerName": "Header Name", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 6bf7b73..31c9e34 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -104,6 +104,9 @@ "enableSelect": "启用选择", "disableSelect": "禁用选择", "deleteSelect": "删除选择", + "testData": "测试数据", + "noChangesDetected": "未检测到变更", + "enterMatchData": "输入待匹配的数据", "modifyRequestHeader": "修改请求头", "headerName": "请求头名称", diff --git a/lib/network/components/rewrite/request_rewrite_manager.dart b/lib/network/components/rewrite/request_rewrite_manager.dart index 4665ecd..fbf2833 100644 --- a/lib/network/components/rewrite/request_rewrite_manager.dart +++ b/lib/network/components/rewrite/request_rewrite_manager.dart @@ -230,7 +230,7 @@ class RequestRewriteManager { } RequestRewriteRule getRequestRewriteRule(HttpRequest request, RuleType type) { - var url = '${request.remoteDomain()}${request.path()}'; + var url = request.domainPath; for (var rule in rules) { if (rule.match(url) && rule.type == type) { return rule; diff --git a/lib/network/components/script_manager.dart b/lib/network/components/script_manager.dart index 1ba04e0..a0ce93d 100644 --- a/lib/network/components/script_manager.dart +++ b/lib/network/components/script_manager.dart @@ -236,7 +236,7 @@ async function onResponse(context, request, response) { if (!enabled) { return request; } - var url = '${request.remoteDomain()}${request.path()}'; + var url = request.domainPath; for (var item in list) { if (item.enabled && item.match(url)) { var context = jsonEncode(scriptContext(item)); @@ -265,7 +265,7 @@ async function onResponse(context, request, response) { } var request = response.request!; - var url = '${request.remoteDomain()}${request.path()}'; + var url = request.domainPath; for (var item in list) { if (item.enabled && item.match(url)) { var context = jsonEncode(request.attributes['scriptContext'] ?? scriptContext(item)); @@ -297,7 +297,6 @@ async function onResponse(context, request, response) { if (jsResult.isPromise || jsResult.rawResult is Future) { jsResult = await flutterJs.handlePromise(jsResult); } - } catch (e) { throw SignalException(jsResult.stringResult); } diff --git a/lib/network/handler.dart b/lib/network/handler.dart index 842076c..65f975a 100644 --- a/lib/network/handler.dart +++ b/lib/network/handler.dart @@ -121,7 +121,7 @@ class HttpProxyChannelHandler extends ChannelHandler { return; } - var uri = '${httpRequest.remoteDomain()}${httpRequest.path()}'; + var uri = httpRequest.domainPath; //脚本替换 var scriptManager = await ScriptManager.instance; HttpRequest? request = await scriptManager.runScript(httpRequest); @@ -282,7 +282,7 @@ class HttpResponseProxyHandler extends ChannelHandler { listener?.onResponse(channelContext, msg); //屏蔽响应 - var uri = '${request?.remoteDomain()}${request?.path()}'; + var uri = request?.domainPath ?? ''; var blockResponse = (await RequestBlockManager.instance).enableBlockResponse(uri); if (blockResponse) { channel.close(); diff --git a/lib/network/http/codec.dart b/lib/network/http/codec.dart index f90f760..44ba307 100644 --- a/lib/network/http/codec.dart +++ b/lib/network/http/codec.dart @@ -156,8 +156,8 @@ abstract class HttpCodec implements Codec { initialLine(builder, message); List? body = message.body; - if (message.headers.isGzip) { - body = gzipEncode(body!); + if (message.headers.isGzip && body != null) { + body = gzipEncode(body); } //请求头 diff --git a/lib/network/http/http.dart b/lib/network/http/http.dart index d80518f..8f08605 100644 --- a/lib/network/http/http.dart +++ b/lib/network/http/http.dart @@ -56,7 +56,7 @@ abstract class HttpMessage { String? remoteHost; int? remotePort; - String requestId = (DateTime.now().millisecondsSinceEpoch + Random().nextInt(99999)).toRadixString(36); + String requestId = (DateTime.now().millisecondsSinceEpoch + Random().nextInt(999999)).toRadixString(36); int? streamId; // http2 streamId HttpMessage(this.protocolVersion); @@ -151,7 +151,11 @@ class HttpRequest extends HttpMessage { } } - String path() { + ///域名+路径 + String get domainPath => '${remoteDomain()}$path'; + + /// 请求的path + String get path { try { var requestPath = Uri.parse(requestUrl).path; return requestPath.isEmpty ? "" : requestPath; diff --git a/lib/network/proxy_helper.dart b/lib/network/proxy_helper.dart index 3f99ac3..0d2fdf3 100644 --- a/lib/network/proxy_helper.dart +++ b/lib/network/proxy_helper.dart @@ -34,7 +34,7 @@ class ProxyHelper { //请求本服务 static localRequest(HttpRequest msg, Channel channel) async { //获取配置 - if (msg.path() == '/config') { + if (msg.path == '/config') { final requestRewrites = await RequestRewriteManager.instance; var response = HttpResponse(HttpStatus.ok, protocolVersion: msg.protocolVersion); var body = { diff --git a/lib/ui/component/json/json_text.dart b/lib/ui/component/json/json_text.dart index bd0ed32..7aed595 100644 --- a/lib/ui/component/json/json_text.dart +++ b/lib/ui/component/json/json_text.dart @@ -13,14 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import 'dart:io'; -import 'dart:math'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:network_proxy/ui/component/json/theme.dart'; +import 'package:network_proxy/ui/component/utils.dart'; -class JsonText extends StatelessWidget { +class JsonText extends StatefulWidget { final ColorTheme colorTheme; final dynamic json; final String indent; @@ -28,6 +26,31 @@ class JsonText extends StatelessWidget { const JsonText({super.key, required this.json, this.indent = ' ', required this.colorTheme, this.scrollController}); + @override + State createState() => _JsonTextSate(); +} + +class _JsonTextSate extends State { + late ColorTheme colorTheme; + late dynamic json; + late String indent; + late ScrollController? scrollController; + + @override + void initState() { + super.initState(); + colorTheme = widget.colorTheme; + json = widget.json; + indent = widget.indent; + scrollController = trackingScroll(widget.scrollController); + } + + @override + void dispose() { + scrollController?.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { var jsnParser = JsnParser(json, colorTheme, indent); @@ -49,7 +72,7 @@ class JsonText extends StatelessWidget { height: MediaQuery.of(context).size.height - 160, child: ListView.builder( physics: const BouncingScrollPhysics(), - controller: trackingScroll(), + controller: scrollController, cacheExtent: 1000, itemBuilder: (context, index) => textList[index], itemCount: textList.length)); @@ -60,32 +83,6 @@ class JsonText extends StatelessWidget { return Container(); }); } - - ///滚动条 - ScrollController trackingScroll() { - var trackingScroll = TrackingScrollController(); - double offset = 0; - trackingScroll.addListener(() { - if (trackingScroll.offset < -10 || (trackingScroll.offset < 30 && trackingScroll.offset < offset)) { - if (scrollController != null && scrollController!.offset >= 0) { - scrollController?.jumpTo(scrollController!.offset - max((offset - trackingScroll.offset), 15)); - } - } - offset = trackingScroll.offset; - }); - - if (Platform.isIOS) { - scrollController?.addListener(() { - if (scrollController!.offset >= scrollController!.position.maxScrollExtent) { - scrollController?.jumpTo(scrollController!.position.maxScrollExtent); - trackingScroll - .jumpTo(trackingScroll.offset + (scrollController!.offset - scrollController!.position.maxScrollExtent)); - } - }); - } - - return trackingScroll; - } } class JsnParser { diff --git a/lib/ui/component/utils.dart b/lib/ui/component/utils.dart index 6d0c24e..24fd7f8 100644 --- a/lib/ui/component/utils.dart +++ b/lib/ui/component/utils.dart @@ -15,6 +15,7 @@ */ import 'dart:io'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -204,3 +205,27 @@ Future showConfirmDialog(BuildContext context, {String? title, String? co ); }); } + +///滚动条 +ScrollController? trackingScroll(ScrollController? scrollController) { + if (scrollController == null) { + return null; + } + + var trackingScroll = TrackingScrollController(); + double offset = 0; + trackingScroll.addListener(() { + if (trackingScroll.offset < 30 && trackingScroll.offset < offset && scrollController.offset > 0) { + //往上滚动 + scrollController.jumpTo(scrollController.offset - max(offset - trackingScroll.offset, 15)); + } else if (trackingScroll.offset > 0 && + trackingScroll.offset > offset && + scrollController.offset < scrollController.position.maxScrollExtent) { + //往下滚动 + scrollController.jumpTo(scrollController.offset + max(trackingScroll.offset - offset, 15)); + } + + offset = trackingScroll.offset; + }); + return trackingScroll; +} diff --git a/lib/ui/content/body.dart b/lib/ui/content/body.dart index 9e09ddf..86836ef 100644 --- a/lib/ui/content/body.dart +++ b/lib/ui/content/body.dart @@ -226,7 +226,8 @@ class HttpBodyState extends State { if (!mounted) return; if (Platforms.isMobile()) { - Navigator.push(context, MaterialPageRoute(builder: (_) => RewriteRule(rule: rule, items: rewriteItems))); + Navigator.push( + context, MaterialPageRoute(builder: (_) => RewriteRule(rule: rule, items: rewriteItems, request: request))); } else { showDialog( context: context, diff --git a/lib/ui/content/panel.dart b/lib/ui/content/panel.dart index 19f9fc8..8da6ea6 100644 --- a/lib/ui/content/panel.dart +++ b/lib/ui/content/panel.dart @@ -285,7 +285,7 @@ class NetworkTabState extends State with SingleTickerProvi } var scrollController = ScrollController(); //处理body也有滚动条问题 - var path = widget.request.get()?.path() ?? ''; + var path = widget.request.get()?.path ?? ''; try { path = Uri.decodeFull(path); } catch (_) {} diff --git a/lib/ui/desktop/request/list.dart b/lib/ui/desktop/request/list.dart index 5a3d0a2..5de9456 100644 --- a/lib/ui/desktop/request/list.dart +++ b/lib/ui/desktop/request/list.dart @@ -149,10 +149,10 @@ class DesktopRequestListState extends State with Autom ///清理 clean() { setState(() { + container.clear(); domainListKey.currentState?.clean(); requestSequenceKey.currentState?.clean(); widget.panel.change(null, null); - container.clear(); }); } diff --git a/lib/ui/desktop/request/request.dart b/lib/ui/desktop/request/request.dart index 80b2c26..eaf6e3d 100644 --- a/lib/ui/desktop/request/request.dart +++ b/lib/ui/desktop/request/request.dart @@ -84,7 +84,7 @@ class _RequestWidgetState extends State { Widget build(BuildContext context) { var request = widget.request; var response = widget.response.get() ?? request.response; - String path = widget.displayDomain ? '${request.remoteDomain()}${request.path()}' : request.path(); + String path = widget.displayDomain ? request.domainPath : request.path; String title = '${request.method.name} $path'; var time = formatDate(request.requestTime, [HH, ':', nn, ':', ss]); @@ -186,7 +186,7 @@ class _RequestWidgetState extends State { label: localizations.script, onClick: (_) async { var scriptManager = await ScriptManager.instance; - var url = '${widget.request.remoteDomain()}${widget.request.path()}'; + var url = widget.request.domainPath; var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.url == url); String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem); diff --git a/lib/ui/desktop/toolbar/setting/request_rewrite.dart b/lib/ui/desktop/toolbar/setting/request_rewrite.dart index 67779f3..f395f85 100644 --- a/lib/ui/desktop/toolbar/setting/request_rewrite.dart +++ b/lib/ui/desktop/toolbar/setting/request_rewrite.dart @@ -460,6 +460,9 @@ class RewriteRuleEdit extends StatefulWidget { } class _RewriteRuleEditState extends State { + final rewriteReplaceKey = GlobalKey(); + final rewriteUpdateKey = GlobalKey(); + late RequestRewriteRule rule; List? items; @@ -467,9 +470,6 @@ class _RewriteRuleEditState extends State { late TextEditingController nameInput; late TextEditingController urlInput; - final rewriteReplaceKey = GlobalKey(); - final rewriteUpdateKey = GlobalKey(); - AppLocalizations get localizations => AppLocalizations.of(context)!; @override @@ -482,7 +482,6 @@ class _RewriteRuleEditState extends State { urlInput = TextEditingController(text: rule.url); if (items == null && widget.request != null) { - print('items == null && widget.request != null'); items = fromRequestItems(widget.request!, ruleType); } } diff --git a/lib/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart b/lib/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart index 613f850..80625a6 100644 --- a/lib/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart +++ b/lib/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart @@ -82,7 +82,7 @@ class RewriteUpdateState extends State { )) ], ), - UpdateList(items: items, ruleType: ruleType), + UpdateList(items: items, ruleType: ruleType, request: widget.request), ], ); } @@ -221,12 +221,15 @@ class _RewriteUpdateAddState extends State { controller: valueController), const SizedBox(height: 10), Row(children: [ - Align(alignment: Alignment.centerLeft, child: Text('测试数据', style: const TextStyle(fontSize: 14))), + Align( + alignment: Alignment.centerLeft, + child: Text(localizations.testData, style: const TextStyle(fontSize: 14))), const SizedBox(width: 10), - if (!isMatch) Text('未检测到变更', style: TextStyle(color: Colors.red, fontSize: 14)) + if (!isMatch) + Text(localizations.noChangesDetected, style: TextStyle(color: Colors.red, fontSize: 14)) ]), const SizedBox(height: 5), - formField('输入待匹配的数据', lines: 10, required: false, controller: dataController), + formField(localizations.enterMatchData, lines: 10, required: false, controller: dataController), ])))); } @@ -294,7 +297,6 @@ class _RewriteUpdateAddState extends State { } var match = dataController.highlight(key, caseSensitive: rewriteType != RewriteType.updateHeader); - print('onChangeMatch $match'); isMatch = match; }); }); @@ -333,8 +335,9 @@ class _RewriteUpdateAddState extends State { class UpdateList extends StatefulWidget { final List items; final RuleType ruleType; + final HttpRequest? request; - const UpdateList({super.key, required this.items, required this.ruleType}); + const UpdateList({super.key, required this.items, required this.ruleType, this.request}); @override State createState() => _UpdateListState(); @@ -383,7 +386,8 @@ class _UpdateListState extends State { hoverColor: primaryColor.withOpacity(0.3), onDoubleTap: () => showDialog( context: context, - builder: (context) => RewriteUpdateAddDialog(item: list[index], ruleType: widget.ruleType)) + builder: (context) => + RewriteUpdateAddDialog(item: list[index], ruleType: widget.ruleType, request: widget.request)) .then((value) { if (value != null) setState(() {}); }), @@ -439,8 +443,8 @@ class _UpdateListState extends State { showDialog( context: context, barrierDismissible: false, - builder: (BuildContext context) => - RewriteUpdateAddDialog(item: widget.items[index], ruleType: widget.ruleType)).then((value) { + builder: (BuildContext context) => RewriteUpdateAddDialog( + item: widget.items[index], ruleType: widget.ruleType, request: widget.request)).then((value) { if (value != null) { setState(() {}); } diff --git a/lib/ui/desktop/toolbar/setting/script.dart b/lib/ui/desktop/toolbar/setting/script.dart index 9592e5b..9b0e3d3 100644 --- a/lib/ui/desktop/toolbar/setting/script.dart +++ b/lib/ui/desktop/toolbar/setting/script.dart @@ -214,7 +214,6 @@ class _ScriptConsoleState extends State { super.initState(); DesktopMultiWindow.invokeMethod(0, "registerConsoleLog", widget.windowId); DesktopMultiWindow.setMethodHandler((call, fromWindowId) async { - // print("consoleLog $scrollEnd $fromWindowId ${call.arguments}"); if (call.method == 'consoleLog') { setState(() { var logInfo = LogInfo(call.arguments['level'], call.arguments['output']); @@ -236,7 +235,6 @@ class _ScriptConsoleState extends State { @override Widget build(BuildContext context) { - // print("script build"); return Scaffold( backgroundColor: Theme.of(context).dialogBackgroundColor, appBar: AppBar( diff --git a/lib/ui/mobile/menu/drawer.dart b/lib/ui/mobile/menu/drawer.dart index b12c0af..621a3c0 100644 --- a/lib/ui/mobile/menu/drawer.dart +++ b/lib/ui/mobile/menu/drawer.dart @@ -92,7 +92,7 @@ class DrawerWidget extends StatelessWidget { onTap: () => navigator(context, FilterMenu(proxyServer: proxyServer))), ListTile( title: Text(localizations.requestRewrite), - leading: const Icon(Icons.replay_outlined), + leading: const Icon(Icons.edit_outlined), onTap: () async { var requestRewrites = await RequestRewriteManager.instance; if (context.mounted) { diff --git a/lib/ui/mobile/menu/me.dart b/lib/ui/mobile/menu/me.dart index 131fa26..0d620b2 100644 --- a/lib/ui/mobile/menu/me.dart +++ b/lib/ui/mobile/menu/me.dart @@ -94,7 +94,7 @@ class _MePageState extends State { onTap: () => navigator(context, FilterMenu(proxyServer: proxyServer))), ListTile( title: Text(localizations.requestRewrite), - leading: Icon(Icons.replay_outlined, color: color), + leading: Icon(Icons.edit_outlined, color: color), trailing: const Icon(Icons.arrow_forward_ios, size: 16), onTap: () async { var requestRewrites = await RequestRewriteManager.instance; diff --git a/lib/ui/mobile/request/favorite.dart b/lib/ui/mobile/request/favorite.dart index f6f7161..3dac4c3 100644 --- a/lib/ui/mobile/request/favorite.dart +++ b/lib/ui/mobile/request/favorite.dart @@ -24,6 +24,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/network/components/rewrite/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/rewrite_rule.dart'; import 'package:network_proxy/network/components/script_manager.dart'; import 'package:network_proxy/network/host_port.dart'; import 'package:network_proxy/network/http/http.dart'; @@ -136,7 +137,7 @@ class _FavoriteItemState extends State<_FavoriteItem> { style: TextStyle(fontSize: 14, color: Colors.blue), ), TextSpan( - text: request.path(), + text: request.path, style: TextStyle(fontSize: 14, color: Colors.green), ), if (request.requestUri?.query.isNotEmpty == true) @@ -249,7 +250,7 @@ class _FavoriteItemState extends State<_FavoriteItem> { Navigator.push(context, pageRoute); }, label: localizations.editRequest, - icon: Icons.edit_outlined), + icon: Icons.replay_outlined), ), //script and rewrite @@ -257,9 +258,8 @@ class _FavoriteItemState extends State<_FavoriteItem> { left: itemButton( onPressed: () async { Navigator.maybePop(context); - var scriptManager = await ScriptManager.instance; - var url = '${request.remoteDomain()}${request.path()}'; + var url = request.domainPath; var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.url == url); String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem); @@ -276,11 +276,17 @@ class _FavoriteItemState extends State<_FavoriteItem> { bool isRequest = request.response == null; var requestRewrites = await RequestRewriteManager.instance; - var pageRoute = MaterialPageRoute(builder: (_) => RewriteRule()); + var ruleType = isRequest ? RuleType.requestReplace : RuleType.responseReplace; + var rule = requestRewrites.getRequestRewriteRule(request, ruleType); + + var rewriteItems = await requestRewrites.getRewriteItems(rule); + + var pageRoute = MaterialPageRoute( + builder: (_) => RewriteRule(rule: rule, items: rewriteItems, request: request)); if (mounted) Navigator.push(context, pageRoute); }, label: localizations.requestRewrite, - icon: Icons.replay_outlined), + icon: Icons.edit_outlined), ), SizedBox(height: 2), Row(mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -362,9 +368,12 @@ class _FavoriteItemState extends State<_FavoriteItem> { Widget itemButton( {required String label, required IconData icon, required Function() onPressed, double iconSize = 20}) { - var style = Theme.of(context).textTheme.bodyMedium; + var theme = Theme.of(context); + var style = theme.textTheme.bodyMedium; return TextButton.icon( - onPressed: onPressed, label: Text(label, style: style), icon: Icon(icon, size: iconSize, color: style?.color)); + onPressed: onPressed, + label: Text(label, style: style), + icon: Icon(icon, size: iconSize, color: theme.colorScheme.primary.withOpacity(0.65))); } Widget menuItem({required Widget left, required Widget right}) { diff --git a/lib/ui/mobile/request/list.dart b/lib/ui/mobile/request/list.dart index 6143318..dde91aa 100644 --- a/lib/ui/mobile/request/list.dart +++ b/lib/ui/mobile/request/list.dart @@ -126,9 +126,9 @@ class RequestListState extends State { ///清理 clean() { setState(() { + container.clear(); domainListKey.currentState?.clean(); requestSequenceKey.currentState?.clean(); - container.clear(); }); } diff --git a/lib/ui/mobile/request/request.dart b/lib/ui/mobile/request/request.dart index 20377c8..6015446 100644 --- a/lib/ui/mobile/request/request.dart +++ b/lib/ui/mobile/request/request.dart @@ -101,7 +101,7 @@ class RequestRowState extends State { @override Widget build(BuildContext context) { - String url = widget.displayDomain ? request.requestUrl : request.path(); + String url = widget.displayDomain ? request.requestUrl : request.path; var title = Strings.autoLineString('${request.method.name} $url'); @@ -250,7 +250,7 @@ class RequestRowState extends State { Navigator.push(getContext(), pageRoute); }, label: localizations.editRequest, - icon: Icons.edit_outlined), + icon: Icons.replay_outlined), ), //script and rewrite menuItem( @@ -259,8 +259,8 @@ class RequestRowState extends State { Navigator.maybePop(availableContext); var scriptManager = await ScriptManager.instance; - var url = '${request.remoteDomain()}${request.path()}'; - var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.url == url); + var url = request.domainPath; + var scriptItem = scriptManager.list.firstWhereOrNull((it) => it.url == url); String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem); var pageRoute = MaterialPageRoute( @@ -278,24 +278,17 @@ class RequestRowState extends State { var requestRewrites = await RequestRewriteManager.instance; var ruleType = isRequest ? RuleType.requestReplace : RuleType.responseReplace; - var url = '${request.remoteDomain()}${request.path()}'; - var rule = requestRewrites.rules.firstWhere((it) => it.matchUrl(url, ruleType), - orElse: () => RequestRewriteRule(type: ruleType, url: url)); + var rule = requestRewrites.getRequestRewriteRule(request, ruleType); var rewriteItems = await requestRewrites.getRewriteItems(rule); - RewriteType rewriteType = - isRequest ? RewriteType.replaceRequestBody : RewriteType.replaceResponseBody; - // if (!rewriteItems?.any((element) => element.type == rewriteType)) { - // rewriteItems.add(RewriteItem(rewriteType, true, - // values: {'body': isRequest ? request.bodyAsString : response?.bodyAsString})); - // } - var pageRoute = MaterialPageRoute(builder: (_) => RewriteRule(rule: rule, items: rewriteItems)); + var pageRoute = MaterialPageRoute( + builder: (_) => RewriteRule(rule: rule, items: rewriteItems, request: request)); var context = availableContext; if (context.mounted) Navigator.push(context, pageRoute); }, label: localizations.requestRewrite, - icon: Icons.replay_outlined), + icon: Icons.edit_outlined), ), menuItem( left: itemButton( @@ -353,9 +346,12 @@ class RequestRowState extends State { Widget itemButton( {required String label, required IconData icon, required Function() onPressed, double iconSize = 20}) { - var style = Theme.of(context).textTheme.bodyMedium; + var theme = Theme.of(context); + var style = theme.textTheme.bodyMedium; return TextButton.icon( - onPressed: onPressed, label: Text(label, style: style), icon: Icon(icon, size: iconSize, color: style?.color)); + onPressed: onPressed, + label: Text(label, style: style), + icon: Icon(icon, size: iconSize, color: theme.colorScheme.primary.withOpacity(0.65))); } Widget menuItem({required Widget left, required Widget right}) { diff --git a/lib/ui/mobile/request/request_sequence.dart b/lib/ui/mobile/request/request_sequence.dart index 7f1931e..88f1f79 100644 --- a/lib/ui/mobile/request/request_sequence.dart +++ b/lib/ui/mobile/request/request_sequence.dart @@ -26,8 +26,8 @@ class RequestSequence extends StatefulWidget { } class RequestSequenceState extends State with AutomaticKeepAliveClientMixin { - ///请求和对应的row的映射 - Map> indexes = HashMap(); + ///请求id和对应的row的映射 + Map> indexes = HashMap(); ///显示的请求列表 最新的在前面 Queue view = Queue(); @@ -71,7 +71,7 @@ class RequestSequenceState extends State with AutomaticKeepAliv ///添加响应 addResponse(HttpResponse response) { - var state = indexes.remove(response.request); + var state = indexes.remove(response.request?.requestId); state?.currentState?.change(response); if (searchModel == null || searchModel!.isEmpty || response.request == null) { @@ -145,10 +145,9 @@ class RequestSequenceState extends State with AutomaticKeepAliv Divider(thickness: 0.2, height: 0, color: Theme.of(context).dividerColor), itemCount: view.length, itemBuilder: (context, index) { - GlobalKey key = indexes[view.elementAt(index)] ??= GlobalKey(); return RequestRow( index: view.length - index, - key: key, + key: indexes[view.elementAt(index).requestId] ??= GlobalKey(), request: view.elementAt(index), proxyServer: widget.proxyServer, displayDomain: widget.displayDomain, diff --git a/lib/ui/mobile/setting/request_rewrite.dart b/lib/ui/mobile/setting/request_rewrite.dart index d511285..31c9317 100644 --- a/lib/ui/mobile/setting/request_rewrite.dart +++ b/lib/ui/mobile/setting/request_rewrite.dart @@ -402,8 +402,9 @@ class _RequestRuleListState extends State { class RewriteRule extends StatefulWidget { final RequestRewriteRule? rule; final List? items; + final HttpRequest? request; - const RewriteRule({super.key, this.rule, this.items}); + const RewriteRule({super.key, this.rule, this.items, this.request}); @override State createState() { @@ -423,6 +424,8 @@ class _RewriteRuleState extends State { AppLocalizations get localizations => AppLocalizations.of(context)!; + final ScrollController scrollController = ScrollController(); + @override void initState() { super.initState(); @@ -432,12 +435,17 @@ class _RewriteRuleState extends State { nameInput = TextEditingController(text: rule.name); urlInput = TextEditingController(text: rule.url); + + if (items == null && widget.request != null) { + items = fromRequestItems(widget.request!, ruleType); + } } @override void dispose() { urlInput.dispose(); nameInput.dispose(); + scrollController.dispose(); super.dispose(); } @@ -491,52 +499,71 @@ class _RewriteRuleState extends State { ), body: Padding( padding: const EdgeInsets.all(15), - child: Form( - key: formKey, - child: ListView(children: [ - Row(children: [ - SizedBox( - width: 60, - child: Text('${localizations.enable}:', - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500))), - SwitchWidget(value: rule.enabled, onChanged: (val) => rule.enabled = val, scale: 0.8) - ]), - textField('${localizations.name}:', nameInput, localizations.pleaseEnter), - textField('URL:', urlInput, 'https://www.example.com/api/*', - required: true, keyboardType: TextInputType.url), - Row(children: [ - SizedBox( - width: 60, - child: Text('${localizations.action}:', - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500))), - SizedBox( - width: 165, - height: 50, - child: DropdownButtonFormField( - onSaved: (val) => rule.type = val!, - value: ruleType, - decoration: const InputDecoration( - errorStyle: TextStyle(height: 0, fontSize: 0), contentPadding: EdgeInsets.only()), - items: RuleType.values - .map((e) => DropdownMenuItem(value: e, child: Text(isCN ? e.label : e.name))) - .toList(), - onChanged: (val) { - setState(() { - ruleType = val!; - items = ruleType == widget.rule?.type ? widget.items : []; - rewriteReplaceKey.currentState?.initItems(ruleType, items); - rewriteUpdateKey.currentState?.initItems(ruleType, items); - }); - }, - )), - const SizedBox(width: 10), - ]), - const SizedBox(height: 10), - rewriteRule() - ])), + child: NestedScrollView( + controller: scrollController, + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverToBoxAdapter( + child: Form( + key: formKey, + child: Column(children: [ + Row(children: [ + SizedBox( + width: 60, + child: Text('${localizations.enable}:', + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500))), + SwitchWidget(value: rule.enabled, onChanged: (val) => rule.enabled = val, scale: 0.8) + ]), + textField('${localizations.name}:', nameInput, localizations.pleaseEnter), + textField('URL:', urlInput, 'https://www.example.com/api/*', + required: true, keyboardType: TextInputType.url), + Row(children: [ + SizedBox( + width: 60, + child: Text('${localizations.action}:', + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500))), + SizedBox( + width: 165, + height: 50, + child: DropdownButtonFormField( + onSaved: (val) => rule.type = val!, + value: ruleType, + decoration: const InputDecoration( + errorStyle: TextStyle(height: 0, fontSize: 0), contentPadding: EdgeInsets.only()), + items: RuleType.values + .map((e) => DropdownMenuItem(value: e, child: Text(isCN ? e.label : e.name))) + .toList(), + onChanged: onChangeType, + )), + const SizedBox(width: 10), + ]), + const SizedBox(height: 10), + ]), + )) + ]; + }, + body: rewriteRule()), )); } + void onChangeType(RuleType? val) async { + if (ruleType == val) return; + + ruleType = val!; + items = []; + + if (ruleType == widget.rule?.type) { + items = widget.items; + } else if (widget.request != null) { + items?.addAll(fromRequestItems(widget.request!, ruleType)); + } + + setState(() { + rewriteReplaceKey.currentState?.initItems(ruleType, items); + rewriteUpdateKey.currentState?.initItems(ruleType, items); + }); + } + static List fromRequestItems(HttpRequest request, RuleType ruleType) { if (ruleType == RuleType.requestReplace) { //请求替换 @@ -550,10 +577,11 @@ class _RewriteRuleState extends State { Widget rewriteRule() { if (ruleType == RuleType.requestUpdate || ruleType == RuleType.responseUpdate) { - return MobileRewriteUpdate(key: rewriteUpdateKey, items: items, ruleType: ruleType); + return MobileRewriteUpdate(key: rewriteUpdateKey, items: items, ruleType: ruleType, request: widget.request); } - return MobileRewriteReplace(key: rewriteReplaceKey, items: items, ruleType: ruleType); + return MobileRewriteReplace( + scrollController: scrollController, key: rewriteReplaceKey, items: items, ruleType: ruleType); } Widget textField(String label, TextEditingController controller, String hint, diff --git a/lib/ui/mobile/setting/rewrite/rewrite_replace.dart b/lib/ui/mobile/setting/rewrite/rewrite_replace.dart index 1c739dd..3577768 100644 --- a/lib/ui/mobile/setting/rewrite/rewrite_replace.dart +++ b/lib/ui/mobile/setting/rewrite/rewrite_replace.dart @@ -20,6 +20,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:network_proxy/network/components/rewrite/rewrite_rule.dart'; import 'package:network_proxy/ui/component/state_component.dart'; +import 'package:network_proxy/ui/component/utils.dart'; import 'package:network_proxy/ui/component/widgets.dart'; import 'package:network_proxy/utils/lang.dart'; @@ -28,8 +29,9 @@ import 'package:network_proxy/utils/lang.dart'; class MobileRewriteReplace extends StatefulWidget { final RuleType ruleType; final List? items; + final ScrollController? scrollController; - const MobileRewriteReplace({super.key, this.items, required this.ruleType}); + const MobileRewriteReplace({super.key, this.items, required this.ruleType, this.scrollController}); @override State createState() => RewriteReplaceState(); @@ -38,6 +40,7 @@ class MobileRewriteReplace extends StatefulWidget { class RewriteReplaceState extends State { final _headerKey = GlobalKey(); final bodyTextController = TextEditingController(); + late ScrollController? bodyScrollController; late RuleType ruleType; List items = []; @@ -48,11 +51,13 @@ class RewriteReplaceState extends State { initState() { super.initState(); initItems(widget.ruleType, widget.items); + bodyScrollController = trackingScroll(widget.scrollController); } @override dispose() { bodyTextController.dispose(); + bodyScrollController?.dispose(); super.dispose(); } @@ -118,19 +123,17 @@ class RewriteReplaceState extends State { ? [localizations.requestLine, localizations.requestHeader, localizations.requestBody] : [localizations.statusCode, localizations.responseHeader, localizations.responseBody]; - return Container( - constraints: BoxConstraints(maxHeight: 660, minHeight: 350), - child: DefaultTabController( - length: tabs.length, - initialIndex: tabs.length - 1, - child: Scaffold( - appBar: tabBar(tabs), - body: TabBarView(children: [ - KeepAliveWrapper(child: requestEdited ? requestLine() : statusCodeEdit()), - KeepAliveWrapper(child: headers()), - KeepAliveWrapper(child: body()) - ])), - )); + return DefaultTabController( + length: tabs.length, + initialIndex: tabs.length - 1, + child: Scaffold( + appBar: tabBar(tabs), + body: TabBarView(children: [ + KeepAliveWrapper(child: requestEdited ? requestLine() : statusCodeEdit()), + KeepAliveWrapper(child: headers()), + KeepAliveWrapper(child: body()) + ])), + ); } return Container(); @@ -157,7 +160,7 @@ class RewriteReplaceState extends State { var rewriteItem = items.firstWhere( (item) => item.type == RewriteType.replaceRequestBody || item.type == RewriteType.replaceResponseBody); - return Column(children: [ + return ListView(physics: const ClampingScrollPhysics(), children: [ Row(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(width: 5), Text("${localizations.type}: "), @@ -196,8 +199,11 @@ class RewriteReplaceState extends State { else TextFormField( controller: bodyTextController, + scrollPhysics: const BouncingScrollPhysics(), + scrollController: bodyScrollController, style: const TextStyle(fontSize: 14), - maxLines: 25, + minLines: 20, + maxLines: 23, decoration: decoration(localizations.replaceBodyWith, hintText: '${localizations.example} {"code":"200","data":{}}'), onChanged: (val) => rewriteItem.body = val) @@ -242,7 +248,7 @@ class RewriteReplaceState extends State { var rewriteItem = items.firstWhere( (item) => item.type == RewriteType.replaceRequestHeader || item.type == RewriteType.replaceResponseHeader); - return Column(children: [ + return ListView(physics: const ClampingScrollPhysics(), children: [ Row(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text('Header'), const SizedBox(width: 10), @@ -258,14 +264,15 @@ class RewriteReplaceState extends State { })) ])) ]), - Headers(headers: rewriteItem.headers, key: _headerKey) + Headers(headers: rewriteItem.headers, key: _headerKey, scrollController: widget.scrollController) ]); } ///请求行 Widget requestLine() { var rewriteItem = items.firstWhere((item) => item.type == RewriteType.replaceRequestLine); - return Column( + return ListView( + physics: const ClampingScrollPhysics(), children: [ Row(children: [ Text(localizations.requestMethod), @@ -303,7 +310,7 @@ class RewriteReplaceState extends State { ]), const SizedBox(height: 15), textField("Path", rewriteItem.path, "${localizations.example} /api/v1/user", - onChanged: (val) => rewriteItem.values['path'] = val), + onChanged: (val) => rewriteItem.path = val), const SizedBox(height: 15), textField("URL${localizations.param}", rewriteItem.queryParam, "${localizations.example} id=1&name=2", onChanged: (val) => rewriteItem.queryParam = val), @@ -347,42 +354,41 @@ class RewriteReplaceState extends State { Widget statusCodeEdit() { var rewriteItem = items.firstWhere((item) => item.type == RewriteType.replaceResponseStatus); - return Column(children: [ - Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(localizations.statusCode), - const SizedBox(width: 10), - SizedBox( - width: 100, - child: TextFormField( - style: const TextStyle(fontSize: 14), - initialValue: rewriteItem.statusCode?.toString(), - onChanged: (val) => rewriteItem.statusCode = int.tryParse(val), - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - decoration: InputDecoration( - contentPadding: const EdgeInsets.all(10), - focusedBorder: focusedBorder(), - isDense: true, - border: const OutlineInputBorder()), - )), - Expanded( - child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - Text(localizations.enable), - const SizedBox(width: 10), - SwitchWidget( - value: rewriteItem.enabled, - scale: 0.65, - onChanged: (val) => setState(() { - rewriteItem.enabled = val; - })) - ])), - const SizedBox(width: 10), - ]) - ]); + return ListView(physics: const ClampingScrollPhysics(), children: [ + Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ + Text(localizations.statusCode), + const SizedBox(width: 10), + SizedBox( + width: 100, + child: TextFormField( + style: const TextStyle(fontSize: 14), + initialValue: rewriteItem.statusCode?.toString(), + onChanged: (val) => rewriteItem.statusCode = int.tryParse(val), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: InputDecoration( + contentPadding: const EdgeInsets.all(10), + focusedBorder: focusedBorder(), + isDense: true, + border: const OutlineInputBorder()), + )), + Expanded( + child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + Text(localizations.enable), + const SizedBox(width: 10), + SwitchWidget( + value: rewriteItem.enabled, + scale: 0.65, + onChanged: (val) => setState(() { + rewriteItem.enabled = val; + })) + ])), + const SizedBox(width: 10), + ]) + ]); } InputDecoration decoration(String label, {String? hintText}) { Color color = Theme.of(context).colorScheme.primary; - // Color color = Colors.blueAccent; return InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: label, @@ -401,8 +407,9 @@ class RewriteReplaceState extends State { ///请求头 class Headers extends StatefulWidget { final Map? headers; + final ScrollController? scrollController; - const Headers({super.key, this.headers}); + const Headers({super.key, this.headers, this.scrollController}); @override State createState() { @@ -465,27 +472,25 @@ class HeadersState extends State with AutomaticKeepAliveClientMixin { Widget build(BuildContext context) { super.build(context); - var list = [ - ..._buildRows(), - ]; - - list.add(TextButton( - child: Text("${localizations.add}Header", textAlign: TextAlign.center), - onPressed: () { - setState(() { - _headers[TextEditingController()] = TextEditingController(); - }); - }, - )); + var list = _buildRows(); return Padding( padding: const EdgeInsets.only(top: 10), child: ListView.separated( shrinkWrap: true, + physics: const ClampingScrollPhysics(), separatorBuilder: (context, index) => index == list.length ? const SizedBox() : const Divider(thickness: 0.2), - itemBuilder: (context, index) => list[index], - itemCount: list.length)); + itemBuilder: (context, index) => index < list.length?list[index] + : TextButton( + child: Text("${localizations.add}Header", textAlign: TextAlign.center), + onPressed: () { + setState(() { + _headers[TextEditingController()] = TextEditingController(); + }); + }, + ), + itemCount: list.length + 1)); } List _buildRows() { @@ -517,7 +522,11 @@ class HeadersState extends State with AutomaticKeepAliveClientMixin { controller: val, minLines: 1, maxLines: 3, - decoration: InputDecoration(isDense: true, border: InputBorder.none, hintText: isKey ? "Key" : "Value"))); + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + hintStyle: TextStyle(fontSize: 12, color: Colors.grey), + hintText: isKey ? "Key" : "Value"))); } Widget _row(Widget key, Widget val, Widget? op) { diff --git a/lib/ui/mobile/setting/rewrite/rewrite_update.dart b/lib/ui/mobile/setting/rewrite/rewrite_update.dart index 0fdf50d..9dcde4f 100644 --- a/lib/ui/mobile/setting/rewrite/rewrite_update.dart +++ b/lib/ui/mobile/setting/rewrite/rewrite_update.dart @@ -18,14 +18,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; import 'package:network_proxy/network/components/rewrite/rewrite_rule.dart'; +import 'package:network_proxy/network/http/http.dart'; +import 'package:network_proxy/ui/component/text_field.dart'; import 'package:network_proxy/ui/component/widgets.dart'; import 'package:network_proxy/utils/lang.dart'; class MobileRewriteUpdate extends StatefulWidget { final RuleType ruleType; final List? items; + final HttpRequest? request; - const MobileRewriteUpdate({super.key, required this.ruleType, this.items}); + const MobileRewriteUpdate({super.key, required this.ruleType, this.items, required this.request}); @override State createState() => RewriteUpdateState(); @@ -61,7 +64,8 @@ class RewriteUpdateState extends State { @override Widget build(BuildContext context) { - return Column( + return ListView( + physics: ClampingScrollPhysics(), children: [ Row( children: [ @@ -76,13 +80,15 @@ class RewriteUpdateState extends State { )) ], ), - UpdateList(items: items, ruleType: ruleType), + UpdateList(items: items, ruleType: ruleType, request: widget.request), ], ); } add() { - showDialog(context: context, builder: (context) => RewriteUpdateAddDialog(ruleType: ruleType)).then((value) { + Navigator.of(context) + .push(MaterialPageRoute(builder: (context) => RewriteUpdateEdit(ruleType: ruleType, request: widget.request))) + .then((value) { if (value != null) { setState(() { items.add(value); @@ -92,21 +98,26 @@ class RewriteUpdateState extends State { } } -class RewriteUpdateAddDialog extends StatefulWidget { +class RewriteUpdateEdit extends StatefulWidget { final RewriteItem? item; final RuleType ruleType; + final HttpRequest? request; - const RewriteUpdateAddDialog({super.key, this.item, required this.ruleType}); + const RewriteUpdateEdit({super.key, this.item, required this.ruleType, this.request}); @override - State createState() => _RewriteUpdateAddState(); + State createState() => _RewriteUpdateAddState(); } -class _RewriteUpdateAddState extends State { +class _RewriteUpdateAddState extends State { late RewriteType rewriteType; GlobalKey formKey = GlobalKey(); late RewriteItem rewriteItem; + var keyController = TextEditingController(); + var valueController = TextEditingController(); + var dataController = HighlightTextEditingController(); + AppLocalizations get i18n => AppLocalizations.of(context)!; @override @@ -114,6 +125,21 @@ class _RewriteUpdateAddState extends State { super.initState(); rewriteType = widget.item?.type ?? RewriteType.updateBody; rewriteItem = widget.item ?? RewriteItem(rewriteType, true); + + keyController.text = rewriteItem.key ?? ''; + valueController.text = rewriteItem.value ?? ''; + + initTestData(); + keyController.addListener(onInputChangeMatch); + dataController.addListener(onInputChangeMatch); + } + + @override + void dispose() { + keyController.dispose(); + valueController.dispose(); + dataController.dispose(); + super.dispose(); } @override @@ -134,84 +160,165 @@ class _RewriteUpdateAddState extends State { var typeList = widget.ruleType == RuleType.requestUpdate ? RewriteType.updateRequest : RewriteType.updateResponse; bool isCN = Localizations.localeOf(context).languageCode == "zh"; - return AlertDialog( - title: Text(i18n.add, - style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500), textAlign: TextAlign.center), - actions: [ - TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(i18n.cancel)), - TextButton( - onPressed: () { - if (!(formKey.currentState as FormState).validate()) { - FlutterToastr.show(i18n.cannotBeEmpty, context, position: FlutterToastr.center); - return; - } - (formKey.currentState as FormState).save(); - rewriteItem.type = rewriteType; - Navigator.of(context).pop(rewriteItem); - }, - child: Text(i18n.confirm)), - ], - content: SizedBox( - height: 243, - child: Form( - key: formKey, - child: ListView(children: [ - Row( - children: [ - Text(i18n.type), - const SizedBox(width: 15), - SizedBox( - width: 140, - child: DropdownButtonFormField( - value: rewriteType, - focusColor: Colors.transparent, - itemHeight: 48, - decoration: const InputDecoration( - contentPadding: EdgeInsets.all(10), isDense: true, border: InputBorder.none), - items: typeList - .map((e) => DropdownMenuItem( - value: e, - child: Text(e.getDescribe(isCN), - style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)))) - .toList(), - onChanged: (val) { - setState(() { - rewriteType = val!; - }); - })), - ], - ), - const SizedBox(height: 15), - textField(isUpdate ? i18n.match : i18n.name, rewriteItem.key, keyTips, - required: !isDelete, onSaved: (val) => rewriteItem.key = val), - const SizedBox(height: 15), - textField(isUpdate ? i18n.replace : i18n.value, rewriteItem.value, valueTips, - onSaved: (val) => rewriteItem.value = val), - ])))); + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text(i18n.requestRewriteRule, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)), + actions: [ + TextButton( + onPressed: () { + if (!(formKey.currentState as FormState).validate()) { + FlutterToastr.show(i18n.cannotBeEmpty, context, position: FlutterToastr.center); + return; + } + (formKey.currentState as FormState).save(); + rewriteItem.key = keyController.text; + rewriteItem.value = valueController.text; + rewriteItem.type = rewriteType; + Navigator.of(context).pop(rewriteItem); + }, + child: Text(i18n.confirm)), + SizedBox(width: 5) + ]), + body: Form( + key: formKey, + child: ListView(padding: const EdgeInsets.all(10), children: [ + Row( + children: [ + Text(i18n.type), + const SizedBox(width: 15), + SizedBox( + width: 140, + child: DropdownButtonFormField( + value: rewriteType, + focusColor: Colors.transparent, + itemHeight: 48, + decoration: const InputDecoration( + contentPadding: EdgeInsets.all(10), isDense: true, border: InputBorder.none), + items: typeList + .map((e) => DropdownMenuItem( + value: e, + child: Text(e.getDescribe(isCN), + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)))) + .toList(), + onChanged: (val) { + setState(() { + rewriteType = val!; + initTestData(); + }); + })), + ], + ), + const SizedBox(height: 15), + textField(isUpdate ? i18n.match : i18n.name, keyTips, controller: keyController, required: !isDelete), + const SizedBox(height: 15), + textField(isUpdate ? i18n.replace : i18n.value, valueTips, controller: valueController), + const SizedBox(height: 10), + Row(children: [ + Align(alignment: Alignment.centerLeft, child: Text(i18n.testData, style: const TextStyle(fontSize: 14))), + const SizedBox(width: 10), + if (!isMatch) Text(i18n.noChangesDetected, style: TextStyle(color: Colors.red, fontSize: 14)) + ]), + const SizedBox(height: 5), + formField(i18n.enterMatchData, lines: 15, required: false, controller: dataController), + ]))); } - Widget textField(String label, String? val, String hint, {bool required = false, FormFieldSetter? onSaved}) { + initTestData() { + dataController.highlightEnabled = rewriteType != RewriteType.addQueryParam && rewriteType != RewriteType.addHeader; + bool isRemove = [RewriteType.removeHeader, RewriteType.removeQueryParam].contains(rewriteType); + + valueController.removeListener(onInputChangeMatch); + if (isRemove) { + valueController.addListener(onInputChangeMatch); + } + + if (widget.request == null) return; + + if (rewriteType == RewriteType.updateBody) { + dataController.text = (widget.ruleType == RuleType.requestUpdate + ? widget.request?.bodyAsString + : widget.request?.response?.bodyAsString) ?? + ''; + return; + } + + if (rewriteType == RewriteType.updateQueryParam || rewriteType == RewriteType.removeQueryParam) { + dataController.text = Uri.decodeQueryComponent(widget.request?.requestUri?.query ?? ''); + return; + } + + if (rewriteType == RewriteType.updateHeader || rewriteType == RewriteType.removeHeader) { + dataController.text = widget.request?.headers.toRawHeaders() ?? ''; + return; + } + + dataController.clear(); + } + + bool onMatch = false; //是否正在匹配 + bool isMatch = true; + + onInputChangeMatch() { + if (onMatch || dataController.highlightEnabled == false) { + return; + } + onMatch = true; + + //高亮显示 + Future.delayed(const Duration(milliseconds: 500), () { + onMatch = false; + if (dataController.text.isEmpty) { + if (isMatch) return; + setState(() { + isMatch = true; + }); + return; + } + + if (!mounted) return; + setState(() { + bool isRemove = [RewriteType.removeHeader, RewriteType.removeQueryParam].contains(rewriteType); + String key = keyController.text; + if (isRemove && key.isNotEmpty) { + if (rewriteType == RewriteType.removeHeader) { + key = '$key: ${valueController.text}'; + } else { + key = '$key=${valueController.text}'; + } + } + + var match = dataController.highlight(key, caseSensitive: rewriteType != RewriteType.updateHeader); + isMatch = match; + }); + }); + } + + Widget textField(String label, String hint, {bool required = false, int? lines, TextEditingController? controller}) { return Row(children: [ SizedBox(width: 55, child: Text(label)), - Expanded( - child: TextFormField( - initialValue: val, - style: const TextStyle(fontSize: 14), - maxLines: 3, - validator: (val) => val?.isNotEmpty == true || !required ? null : "", - onSaved: onSaved, - decoration: InputDecoration( - hintText: hint, - hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14), - contentPadding: const EdgeInsets.all(10), - errorStyle: const TextStyle(height: 0, fontSize: 0), - focusedBorder: focusedBorder(), - isDense: true, - border: const OutlineInputBorder()), - )) + Expanded(child: formField(hint, required: required, lines: lines, controller: controller)) ]); } + Widget formField(String hint, {bool required = false, int? lines, TextEditingController? controller}) { + return TextFormField( + controller: controller, + style: const TextStyle(fontSize: 14), + minLines: lines ?? 1, + maxLines: lines ?? 3, + validator: (val) => val?.isNotEmpty == true || !required ? null : "", + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14), + contentPadding: const EdgeInsets.all(10), + errorStyle: const TextStyle(height: 0, fontSize: 0), + focusedBorder: focusedBorder(), + isDense: true, + border: const OutlineInputBorder()), + ); + } + InputBorder focusedBorder() { return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2)); } @@ -220,8 +327,9 @@ class _RewriteUpdateAddState extends State { class UpdateList extends StatefulWidget { final List items; final RuleType ruleType; + final HttpRequest? request; - const UpdateList({super.key, required this.items, required this.ruleType}); + const UpdateList({super.key, required this.items, required this.ruleType, this.request}); @override State createState() => _UpdateListState(); @@ -239,10 +347,8 @@ class _UpdateListState extends State { Widget build(BuildContext context) { return Container( padding: const EdgeInsets.only(top: 10), - constraints: const BoxConstraints(minHeight: 350), decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))), - child: SingleChildScrollView( - child: Column(children: [ + child: Column(children: [ Row( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -254,7 +360,7 @@ class _UpdateListState extends State { ), const Divider(thickness: 0.5), Column(children: rows(widget.items)) - ]))); + ])); } int selected = -1; @@ -267,9 +373,11 @@ class _UpdateListState extends State { highlightColor: Colors.transparent, splashColor: Colors.transparent, hoverColor: primaryColor.withOpacity(0.3), - onTap: () => showDialog( - context: context, - builder: (context) => RewriteUpdateAddDialog(item: list[index], ruleType: widget.ruleType)) + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + RewriteUpdateEdit(item: list[index], ruleType: widget.ruleType, request: widget.request))) .then((value) { if (value != null) setState(() {}); }), @@ -326,12 +434,14 @@ class _UpdateListState extends State { return Wrap(alignment: WrapAlignment.center, children: [ BottomSheetItem( text: i18n.modify, - onPressed: () async { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) => - RewriteUpdateAddDialog(item: widget.items[index], ruleType: widget.ruleType)).then((value) { + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => RewriteUpdateEdit( + item: widget.items[index], + ruleType: widget.ruleType, + request: widget.request))).then((value) { if (value != null) { setState(() {}); }