From 11666ea9fc3e22e0b3c3d76077f5415fa0356d0e Mon Sep 17 00:00:00 2001 From: wanghongenpin Date: Sun, 20 Oct 2024 13:22:58 +0800 Subject: [PATCH] Popup menu UI optimization & request highlighting --- lib/l10n/app_en.arb | 1 + lib/l10n/app_zh.arb | 1 + lib/ui/component/qrcode/qr_scan_view.dart | 19 +- lib/ui/component/share.dart | 5 +- lib/ui/content/panel.dart | 19 -- lib/ui/mobile/request/favorite.dart | 278 ++++++++++++++------ lib/ui/mobile/request/history.dart | 3 +- lib/ui/mobile/request/request.dart | 125 +++++++-- lib/ui/mobile/request/request_sequence.dart | 3 +- lib/ui/mobile/setting/filter.dart | 86 +++--- lib/ui/mobile/widgets/about.dart | 12 +- lib/ui/mobile/widgets/remote_device.dart | 9 +- pubspec.yaml | 2 +- 13 files changed, 366 insertions(+), 197 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index cc31ab0..832b94e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -275,6 +275,7 @@ "iosVpnBackgroundAudio": "After turning on packet capture, exit to the background. In order to maintain the main UI thread for network communication, a silent audio playback will be enabled to keep the main thread running. Otherwise, it will only run in the background for 30 seconds. Do you agree to play audio in the background after turning on packet capture?", + "markRead": "Mark as read", "highlight": "Highlight", "blue" : "Blue", "green" : "Green", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index db48564..a59998a 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -274,6 +274,7 @@ "material3": "Material3是谷歌开源设计系统的最新版本", "iosVpnBackgroundAudio": "开启抓包后,退出到后台。为了维护主UI线程的网络通信,将启用静音音频播放以保持主线程运行。否则,它将只在后台运行30秒。您同意在启用抓包后在后台播放音频吗?", + "markRead": "标记已读", "highlight": "高亮", "blue": "蓝色", "green": "绿色", diff --git a/lib/ui/component/qrcode/qr_scan_view.dart b/lib/ui/component/qrcode/qr_scan_view.dart index 5a10a07..e405820 100644 --- a/lib/ui/component/qrcode/qr_scan_view.dart +++ b/lib/ui/component/qrcode/qr_scan_view.dart @@ -52,7 +52,7 @@ class QeCodeScanView extends StatefulWidget { } class _QrReaderViewState extends State with TickerProviderStateMixin { - final int animationTime = 1500; + final int animationTime = 2000; QrReaderViewController? _controller; AnimationController? _animationController; @@ -96,22 +96,21 @@ class _QrReaderViewState extends State with TickerProviderStateM ..addStatusListener((state) { if (!mounted) { stop(); - return; } if (state == AnimationStatus.completed) { Future.delayed(Duration(seconds: 1), () { - _animationController?.reverse(from: 1.5); + _animationController?.reverse(); }); } else if (state == AnimationStatus.dismissed) { Future.delayed(Duration(seconds: 1), () { - _animationController?.forward(from: 1.5); + _animationController?.forward(); }); } }); - _animationController?.forward(from: 0.0); + _animationController?.forward(); } void stop() { @@ -132,10 +131,12 @@ class _QrReaderViewState extends State with TickerProviderStateM setState(() {}); } - Future setFlashlight() async { - openFlashlight = await _controller?.setFlashlight() ?? false; - setState(() {}); - return openFlashlight; + setFlashlight() async { + if (!isScan) return false; + _controller?.setFlashlight(); + setState(() { + openFlashlight = !openFlashlight; + }); } scanImage(String path) { diff --git a/lib/ui/component/share.dart b/lib/ui/component/share.dart index d989141..cf258c7 100644 --- a/lib/ui/component/share.dart +++ b/lib/ui/component/share.dart @@ -27,6 +27,7 @@ class ShareWidget extends StatelessWidget { itemBuilder: (BuildContext context) { return [ PopupMenuItem( + padding: const EdgeInsets.only(left: 10, right: 2), child: Text(localizations.shareUrl), onTap: () { if (request == null) { @@ -37,7 +38,7 @@ class ShareWidget extends StatelessWidget { }, ), PopupMenuItem( - padding: const EdgeInsets.only(left: 10), + padding: const EdgeInsets.only(left: 10, right: 2), child: Text(localizations.shareRequestResponse), onTap: () { if (request == null) { @@ -49,7 +50,7 @@ class ShareWidget extends StatelessWidget { Share.shareXFiles([file], fileNameOverrides: ['request.txt'], text: localizations.proxyPinSoftware); }), PopupMenuItem( - padding: const EdgeInsets.only(left: 10), + padding: const EdgeInsets.only(left: 10, right: 2), child: Text(localizations.shareCurl), onTap: () { if (request == null) { diff --git a/lib/ui/content/panel.dart b/lib/ui/content/panel.dart index 1527fa4..9b14eb4 100644 --- a/lib/ui/content/panel.dart +++ b/lib/ui/content/panel.dart @@ -18,7 +18,6 @@ import 'package:flutter/services.dart'; 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/script_manager.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/http/websocket.dart'; import 'package:network_proxy/storage/favorites.dart'; @@ -28,7 +27,6 @@ import 'package:network_proxy/ui/component/utils.dart'; import 'package:network_proxy/ui/component/widgets.dart'; import 'package:network_proxy/ui/configuration.dart'; import 'package:network_proxy/ui/mobile/request/request_editor.dart'; -import 'package:network_proxy/ui/mobile/setting/script.dart'; import 'package:network_proxy/utils/lang.dart'; import 'package:network_proxy/utils/platform.dart'; import 'package:network_proxy/utils/python.dart'; @@ -146,23 +144,6 @@ class NetworkTabState extends State with SingleTickerProvi request: widget.request.get(), proxyServer: widget.proxyServer))); }); }), - PopupMenuItem( - child: Text(localizations.script), - onTap: () { - WidgetsBinding.instance.addPostFrameCallback((_) async { - var scriptManager = await ScriptManager.instance; - var request = widget.request.get(); - var url = '${request?.remoteDomain()}${request?.path()}'; - var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.url == url); - String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem); - - if (!context.mounted) return; - - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => ScriptEdit( - scriptItem: scriptItem, script: script, url: scriptItem?.url ?? url))); - }); - }), CustomPopupMenuItem( padding: const EdgeInsets.only(left: 10), child: Text(localizations.copyAsPythonRequests), diff --git a/lib/ui/mobile/request/favorite.dart b/lib/ui/mobile/request/favorite.dart index 6cb9d35..675b47d 100644 --- a/lib/ui/mobile/request/favorite.dart +++ b/lib/ui/mobile/request/favorite.dart @@ -22,14 +22,19 @@ import 'package:flutter/services.dart'; 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/request_rewrite_manager.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'; 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/ui/mobile/request/repeat.dart'; import 'package:network_proxy/ui/mobile/request/request_editor.dart'; +import 'package:network_proxy/ui/mobile/setting/request_rewrite.dart'; +import 'package:network_proxy/ui/mobile/setting/script.dart'; import 'package:network_proxy/utils/curl.dart'; import 'package:network_proxy/utils/lang.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -100,6 +105,7 @@ class _FavoriteItem extends StatefulWidget { class _FavoriteItemState extends State<_FavoriteItem> { late HttpRequest request; + bool selected = false; AppLocalizations get localizations => AppLocalizations.of(context)!; @@ -125,8 +131,13 @@ class _FavoriteItemState extends State<_FavoriteItem> { TextSpan(children: [ TextSpan(text: '${request.method.name} ', style: const TextStyle(fontSize: 14, color: Colors.teal)), TextSpan( - text: '${request.remoteDomain()}${request.path()}'.fixAutoLines(), - style: TextStyle(fontSize: 14, color: Colors.blueAccent.shade200)), + text: request.remoteDomain(), + style: TextStyle(fontSize: 14, color: Colors.blue), + ), + TextSpan( + text: request.path(), + style: TextStyle(fontSize: 14, color: Colors.green), + ), if (request.requestUri?.query.isNotEmpty == true) TextSpan( text: '?${request.requestUri?.query}', @@ -136,83 +147,171 @@ class _FavoriteItemState extends State<_FavoriteItem> { var time = formatDate(request.requestTime, [mm, '-', d, ' ', HH, ':', nn, ':', ss]); String subtitle = '$time - [${response?.status.code ?? ''}] ${response?.contentType.name.toUpperCase() ?? ''} ${response?.costTime() ?? ''} '; - return ListTile( - onLongPress: menu, - minLeadingWidth: 25, - leading: getIcon(response), - title: title, - subtitle: Text.rich( - maxLines: 1, - TextSpan(children: [ - TextSpan(text: '#${widget.index} ', style: const TextStyle(fontSize: 12, color: Colors.teal)), - TextSpan(text: subtitle, style: const TextStyle(fontSize: 12)), - ])), - dense: true, - onTap: onClick); + + return GestureDetector( + onLongPressStart: menu, + child: ListTile( + selected: selected, + minLeadingWidth: 25, + leading: getIcon(response), + title: title, + subtitle: Text.rich( + maxLines: 1, + TextSpan(children: [ + TextSpan(text: '#${widget.index} ', style: const TextStyle(fontSize: 12, color: Colors.teal)), + TextSpan(text: subtitle, style: const TextStyle(fontSize: 12)), + ])), + dense: true, + onTap: onClick)); } ///右键菜单 - menu() { - showModalBottomSheet( - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))), - context: context, - isScrollControlled: true, - builder: (ctx) { - return Wrap(alignment: WrapAlignment.center, children: [ - menuItem(localizations.copyUrl, () => request.requestUrl), - const Divider(thickness: 0.5, height: 5), - menuItem(localizations.copyCurl, () => curlRequest(request)), - const Divider(thickness: 0.5, height: 5), - TextButton( - child: SizedBox(width: double.infinity, child: Text(localizations.rename, textAlign: TextAlign.center)), - onPressed: () { - Navigator.of(context).pop(); - rename(widget.favorite); - }), - const Divider(thickness: 0.5, height: 5), - TextButton( - child: SizedBox(width: double.infinity, child: Text(localizations.repeat, textAlign: TextAlign.center)), - onPressed: () { - onRepeat(request); - Navigator.of(context).pop(); - }), - const Divider(thickness: 0.5, height: 5), - TextButton( - child: SizedBox( - width: double.infinity, child: Text(localizations.customRepeat, textAlign: TextAlign.center)), - onPressed: () => showCustomRepeat(request)), - const Divider(thickness: 0.5, height: 5), - TextButton( - child: - SizedBox(width: double.infinity, child: Text(localizations.editRequest, textAlign: TextAlign.center)), - onPressed: () { - Navigator.of(context).pop(); - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => MobileRequestEditor(request: request, proxyServer: widget.proxyServer))); - }), - const Divider(thickness: 0.5, height: 5), - TextButton( - child: SizedBox( - width: double.infinity, child: Text(localizations.deleteFavorite, textAlign: TextAlign.center)), - onPressed: () { - widget.onRemove?.call(widget.favorite); - FlutterToastr.show(localizations.deleteSuccess, context); - Navigator.of(context).pop(); - }), - Container(color: Theme.of(context).hoverColor, height: 8), - TextButton( - child: Container( - height: 45, - width: double.infinity, - padding: const EdgeInsets.only(top: 10), - child: Text(localizations.cancel, textAlign: TextAlign.center)), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ]); - }, - ); + menu(details) { + setState(() { + selected = true; + }); + + var globalPosition = details.globalPosition; + MediaQueryData mediaQuery = MediaQuery.of(context); + var position = RelativeRect.fromLTRB(globalPosition.dx, globalPosition.dy, globalPosition.dx, globalPosition.dy); + // Trigger haptic feedback + HapticFeedback.mediumImpact(); + + showMenu( + context: context, + constraints: BoxConstraints(maxWidth: mediaQuery.size.width * 0.88), + position: position, + items: [ + //复制url + PopupMenuContainer( + child: Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.only(left: 20, top: 5), + child: Text(localizations.selectAction, style: Theme.of(context).textTheme.bodyLarge)), + ), + //copy + menuItem( + left: itemButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: request.requestUrl)).then((value) { + if (mounted) { + FlutterToastr.show(localizations.copied, context); + Navigator.maybePop(context); + } + }); + }, + label: localizations.copyUrl, + icon: Icons.link, + iconSize: 22), + right: itemButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: curlRequest(request))).then((value) { + if (mounted) { + FlutterToastr.show(localizations.copied, context); + Navigator.maybePop(context); + } + }); + }, + label: localizations.copyCurl, + icon: Icons.code), + ), + //repeat + menuItem( + left: itemButton( + onPressed: () { + onRepeat(request); + Navigator.maybePop(context); + }, + label: localizations.repeat, + icon: Icons.repeat_one), + right: itemButton( + onPressed: () => showCustomRepeat(request), label: localizations.customRepeat, icon: Icons.repeat), + ), + //favorite and edit + menuItem( + left: itemButton( + onPressed: () { + Navigator.of(context).pop(); + rename(widget.favorite); + }, + label: localizations.rename, + icon: Icons.drive_file_rename_outline), + right: itemButton( + onPressed: () async { + Navigator.pop(context); + + var pageRoute = MaterialPageRoute( + builder: (context) => MobileRequestEditor(request: request, proxyServer: widget.proxyServer)); + Navigator.push(context, pageRoute); + }, + label: localizations.editRequest, + icon: Icons.edit_outlined), + ), + + //script and rewrite + menuItem( + left: itemButton( + onPressed: () async { + Navigator.maybePop(context); + + var scriptManager = await ScriptManager.instance; + var url = '${request.remoteDomain()}${request.path()}'; + var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.url == url); + String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem); + + var pageRoute = MaterialPageRoute( + builder: (context) => + ScriptEdit(scriptItem: scriptItem, script: script, url: scriptItem?.url ?? url)); + if (mounted) Navigator.push(context, pageRoute); + }, + label: localizations.script, + icon: Icons.javascript_outlined), + right: itemButton( + onPressed: () async { + Navigator.maybePop(context); + bool isRequest = request.response == null; + var requestRewrites = await RequestRewrites.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 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 : request.response?.bodyAsString})); + } + + var pageRoute = MaterialPageRoute(builder: (_) => RewriteRule(rule: rule, items: rewriteItems)); + if (mounted) Navigator.push(context, pageRoute); + }, + label: localizations.requestRewrite, + icon: Icons.replay_outlined), + ), + SizedBox(height: 2), + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + itemButton( + onPressed: () { + widget.onRemove?.call(widget.favorite); + FlutterToastr.show(localizations.deleteSuccess, context); + Navigator.maybePop(context); + }, + label: localizations.deleteFavorite, + icon: Icons.delete_outline), + SizedBox(width: 10), + ]), + ], + )), + ]).then((value) { + selected = false; + if (mounted) setState(() {}); + }); } //显示高级重发 @@ -233,17 +332,6 @@ class _FavoriteItemState extends State<_FavoriteItem> { } } - Widget menuItem(String title, String Function() callback) { - return TextButton( - child: SizedBox(width: double.infinity, child: Text(title, textAlign: TextAlign.center)), - onPressed: () { - Clipboard.setData(ClipboardData(text: callback.call())).then((value) { - FlutterToastr.show(localizations.copied, context); - Navigator.of(context).pop(); - }); - }); - } - //重命名 rename(Favorite item) { String? name = item.name; @@ -283,4 +371,20 @@ class _FavoriteItemState extends State<_FavoriteItem> { title: Text(localizations.captureDetail, style: const TextStyle(fontSize: 16))); })); } + + Widget itemButton( + {required String label, required IconData icon, required Function() onPressed, double iconSize = 20}) { + var style = Theme.of(context).textTheme.bodyMedium; + return TextButton.icon( + onPressed: onPressed, label: Text(label, style: style), icon: Icon(icon, size: iconSize, color: style?.color)); + } + + Widget menuItem({required Widget left, required Widget right}) { + return Row( + children: [ + SizedBox(width: 130, child: Align(alignment: Alignment.centerLeft, child: left)), + Expanded(child: Align(alignment: Alignment.centerLeft, child: right)) + ], + ); + } } diff --git a/lib/ui/mobile/request/history.dart b/lib/ui/mobile/request/history.dart index e3ee35a..fdf95fa 100644 --- a/lib/ui/mobile/request/history.dart +++ b/lib/ui/mobile/request/history.dart @@ -181,7 +181,8 @@ class _MobileHistoryState extends State { Widget buildItem(HistoryStorage storage, int index, HistoryItem item) { return InkWell( onTapDown: (detail) async { - HapticFeedback.heavyImpact(); + HapticFeedback.mediumImpact(); + showContextMenu(context, detail.globalPosition.translate(-50, index == 0 ? -100 : 100), items: [ PopupMenuItem(child: Text(localizations.rename), onTap: () => renameHistory(storage, item)), PopupMenuItem(child: Text(localizations.share), onTap: () => export(storage, item)), diff --git a/lib/ui/mobile/request/request.dart b/lib/ui/mobile/request/request.dart index 95dee40..db875a4 100644 --- a/lib/ui/mobile/request/request.dart +++ b/lib/ui/mobile/request/request.dart @@ -21,6 +21,8 @@ import 'package:flutter/services.dart'; 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/request_rewrite_manager.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'; import 'package:network_proxy/network/http_client.dart'; @@ -31,6 +33,8 @@ import 'package:network_proxy/ui/component/widgets.dart'; import 'package:network_proxy/ui/content/panel.dart'; import 'package:network_proxy/ui/mobile/request/repeat.dart'; import 'package:network_proxy/ui/mobile/request/request_editor.dart'; +import 'package:network_proxy/ui/mobile/setting/request_rewrite.dart'; +import 'package:network_proxy/ui/mobile/setting/script.dart'; import 'package:network_proxy/ui/mobile/widgets/highlight.dart'; import 'package:network_proxy/utils/curl.dart'; import 'package:network_proxy/utils/lang.dart'; @@ -65,6 +69,7 @@ class RequestRowState extends State { late HttpRequest request; HttpResponse? response; bool selected = false; + Color? highlightColor; //高亮颜色 AppLocalizations get localizations => AppLocalizations.of(context)!; @@ -81,6 +86,14 @@ class RequestRowState extends State { super.initState(); } + Color? color(String url) { + if (highlightColor != null) { + return highlightColor; + } + + return KeywordHighlight.getHighlightColor(url); + } + @override Widget build(BuildContext context) { String url = widget.displayDomain ? request.requestUrl : request.path(); @@ -93,7 +106,7 @@ class RequestRowState extends State { var subTitle = '$time - [${response?.status.code ?? ''}] $contentType $packagesSize ${response?.costTime() ?? ''}'; - var highlightColor = KeywordHighlight.getHighlightColor(url); + var highlightColor = color(url); return GestureDetector( onLongPressStart: menu, @@ -115,7 +128,6 @@ class RequestRowState extends State { contentPadding: Platform.isIOS ? const EdgeInsets.symmetric(horizontal: 8) : const EdgeInsets.only(left: 3, right: 5), onTap: () { - Navigator.of(this.context).push(MaterialPageRoute(builder: (context) { return NetworkTabController( proxyServer: widget.proxyServer, @@ -163,6 +175,7 @@ class RequestRowState extends State { var position = RelativeRect.fromLTRB(globalPosition.dx, globalPosition.dy, globalPosition.dx, globalPosition.dy); // Trigger haptic feedback HapticFeedback.mediumImpact(); + showMenu( context: context, constraints: BoxConstraints(maxWidth: mediaQuery.size.width * 0.88), @@ -180,7 +193,7 @@ class RequestRowState extends State { ), //copy menuItem( - left: button( + left: itemButton( onPressed: () { Clipboard.setData(ClipboardData(text: request.requestUrl)).then((value) { if (mounted) { @@ -192,7 +205,7 @@ class RequestRowState extends State { label: localizations.copyUrl, icon: Icons.link, iconSize: 22), - right: button( + right: itemButton( onPressed: () { Clipboard.setData(ClipboardData(text: curlRequest(request))).then((value) { if (mounted) { @@ -206,19 +219,19 @@ class RequestRowState extends State { ), //repeat menuItem( - left: button( + left: itemButton( onPressed: () { onRepeat(request); Navigator.maybePop(context); }, label: localizations.repeat, icon: Icons.repeat_one), - right: button( + right: itemButton( onPressed: () => showCustomRepeat(request), label: localizations.customRepeat, icon: Icons.repeat), ), //favorite and edit menuItem( - left: button( + left: itemButton( onPressed: () { FavoriteStorage.addFavorite(widget.request); FlutterToastr.show(localizations.addSuccess, context); @@ -226,9 +239,9 @@ class RequestRowState extends State { }, label: localizations.favorite, icon: Icons.favorite_outline), - right: button( - onPressed: () { - Navigator.pop(context); + right: itemButton( + onPressed: () async { + await Navigator.maybePop(context); var pageRoute = MaterialPageRoute( builder: (context) => @@ -242,24 +255,77 @@ class RequestRowState extends State { label: localizations.editRequest, icon: Icons.edit_outlined), ), + //script and rewrite + menuItem( + left: itemButton( + onPressed: () async { + Navigator.maybePop(context); - // menuItem( - // left: button( - // onPressed: () {}, label: localizations.script, icon: Icons.javascript_outlined, iconSize: 24), - // right: button(onPressed: () {}, label: localizations.requestRewrite, icon: Icons.replay_outlined), - // ), - // menuItem( - // left: TextButton.icon( - // onPressed: () {}, - // label: Text(localizations.highlight), - // icon: const Icon(Icons.highlight_outlined, size: 20)), - // right: TextButton.icon( - // onPressed: () {}, - // label: Text(localizations.requestRewrite), - // icon: const Icon(Icons.highlight_remove, size: 20)), - // ), + var scriptManager = await ScriptManager.instance; + var url = '${request.remoteDomain()}${request.path()}'; + var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.url == url); + String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem); + + var pageRoute = MaterialPageRoute( + builder: (context) => + ScriptEdit(scriptItem: scriptItem, script: script, url: scriptItem?.url ?? url)); + if (mounted) { + Navigator.push(context, pageRoute); + } else { + NavigatorHelper.push(pageRoute); + } + }, + label: localizations.script, + icon: Icons.javascript_outlined), + right: itemButton( + onPressed: () async { + Navigator.maybePop(context); + bool isRequest = response == null; + var requestRewrites = await RequestRewrites.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 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)); + + if (mounted) { + Navigator.push(context, pageRoute); + } else { + NavigatorHelper.push(pageRoute); + } + }, + label: localizations.requestRewrite, + icon: Icons.replay_outlined), + ), + menuItem( + left: itemButton( + onPressed: () { + highlightColor = Theme.of(context).colorScheme.primary; + Navigator.maybePop(context); + }, + label: localizations.highlight, + icon: Icons.highlight_outlined), + right: itemButton( + onPressed: () { + highlightColor = Colors.grey; + Navigator.maybePop(context); + }, + label: localizations.markRead, + icon: Icons.mark_chat_read_outlined), + ), + SizedBox(height: 2), Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - button( + itemButton( onPressed: () { widget.onRemove?.call(request); FlutterToastr.show(localizations.deleteSuccess, context); @@ -278,8 +344,8 @@ class RequestRowState extends State { } //显示高级重发 - showCustomRepeat(HttpRequest request) { - Navigator.pop(context); + showCustomRepeat(HttpRequest request) async { + await Navigator.maybePop(context); var pageRoute = MaterialPageRoute( builder: (context) => futureWidget(SharedPreferences.getInstance(), (prefs) => MobileCustomRepeat(onRepeat: () => onRepeat(request), prefs: prefs))); @@ -300,7 +366,8 @@ class RequestRowState extends State { } } - Widget button({required String label, required IconData icon, required Function() onPressed, double iconSize = 20}) { + Widget itemButton( + {required String label, required IconData icon, required Function() onPressed, double iconSize = 20}) { var style = Theme.of(context).textTheme.bodyMedium; return TextButton.icon( onPressed: onPressed, label: Text(label, style: style), icon: Icon(icon, size: iconSize, color: style?.color)); diff --git a/lib/ui/mobile/request/request_sequence.dart b/lib/ui/mobile/request/request_sequence.dart index 2c547f0..38bcdad 100644 --- a/lib/ui/mobile/request/request_sequence.dart +++ b/lib/ui/mobile/request/request_sequence.dart @@ -143,8 +143,7 @@ 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 = GlobalKey(); - indexes[view.elementAt(index)] = key; + GlobalKey key = indexes[view.elementAt(index)] ??= GlobalKey(); return RequestRow( index: view.length - index, key: key, diff --git a/lib/ui/mobile/setting/filter.dart b/lib/ui/mobile/setting/filter.dart index c2fde93..4192c59 100644 --- a/lib/ui/mobile/setting/filter.dart +++ b/lib/ui/mobile/setting/filter.dart @@ -17,6 +17,7 @@ import 'dart:collection'; import 'dart:convert'; import 'package:file_selector/file_selector.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -24,7 +25,6 @@ import 'package:flutter_toastr/flutter_toastr.dart'; import 'package:network_proxy/network/bin/configuration.dart'; import 'package:network_proxy/network/util/logger.dart'; import 'package:network_proxy/ui/component/utils.dart'; -import 'package:network_proxy/ui/component/widgets.dart'; import 'package:share_plus/share_plus.dart'; import '../../../network/components/host_filter.dart'; @@ -312,10 +312,12 @@ class _DomainListState extends State { return List.generate(list.length, (index) { return InkWell( + enableFeedback: false, highlightColor: Colors.transparent, splashColor: Colors.transparent, hoverColor: primaryColor.withOpacity(0.3), - onLongPress: () => showMenus(index), // menus + onLongPress: () => showMenus(index), + // menus onDoubleTap: () => showEdit(index), onTap: () { if (multiple) { @@ -366,45 +368,51 @@ class _DomainListState extends State { setState(() { selected.add(index); }); + HapticFeedback.mediumImpact(); - showModalBottomSheet( - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))), + showCupertinoModalPopup( context: context, - enableDrag: true, - builder: (ctx) { - return Wrap(alignment: WrapAlignment.center, children: [ - BottomSheetItem( - text: localizations.multiple, - onPressed: () { - setState(() => multiple = true); - }), - const Divider(thickness: 0.5, height: 5), - BottomSheetItem(text: localizations.copy, onPressed: () { - Clipboard.setData(ClipboardData(text: widget.hostList.list[index].pattern.replaceAll(".*", "*"))); - FlutterToastr.show(localizations.copied, context); - }), - const Divider(thickness: 0.5, height: 5), - BottomSheetItem(text: localizations.edit, onPressed: () => showEdit(index)), - const Divider(thickness: 0.5, height: 5), - BottomSheetItem(onPressed: () => export([index]), text: localizations.share), - const Divider(thickness: 0.5, height: 5), - BottomSheetItem( - text: localizations.delete, - onPressed: () { - widget.hostList.removeIndex([index]); - onChanged(); - }), - Container(color: Theme.of(context).hoverColor, height: 8), - TextButton( - child: Container( - height: 45, - width: double.infinity, - padding: const EdgeInsets.only(top: 10), - child: Text(localizations.cancel, textAlign: TextAlign.center)), - onPressed: () { - Navigator.of(context).pop(); - }), - ]); + builder: (BuildContext context) { + return CupertinoActionSheet( + actions: [ + CupertinoActionSheetAction( + child: Text(localizations.multiple), + onPressed: () { + setState(() => multiple = true); + Navigator.of(context).pop(); + }), + CupertinoActionSheetAction( + onPressed: () { + Clipboard.setData(ClipboardData(text: widget.hostList.list[index].pattern.replaceAll(".*", "*"))); + FlutterToastr.show(localizations.copied, context); + Navigator.of(context).pop(); + }, + child: Text(localizations.copy)), + CupertinoActionSheetAction( + onPressed: () { + Navigator.of(context).pop(); + showEdit(index); + }, + child: Text(localizations.edit)), + CupertinoActionSheetAction( + onPressed: () { + export([index]); + Navigator.of(context).pop(); + }, + child: Text(localizations.share)), + CupertinoActionSheetAction( + onPressed: () { + widget.hostList.removeIndex([index]); + onChanged(); + Navigator.of(context).pop(); + }, + child: Text(localizations.delete)), + ], + cancelButton: CupertinoActionSheetAction( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(localizations.cancel))); }).then((value) { if (multiple) { return; diff --git a/lib/ui/mobile/widgets/about.dart b/lib/ui/mobile/widgets/about.dart index 5b7b196..3830995 100644 --- a/lib/ui/mobile/widgets/about.dart +++ b/lib/ui/mobile/widgets/about.dart @@ -27,6 +27,7 @@ class About extends StatelessWidget { AppLocalizations localizations = AppLocalizations.of(context)!; bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh'); + String gitHub = "https://github.com/wanghongenpin/network_proxy_flutter"; return Scaffold( appBar: AppBar(title: Text(localizations.about, style: const TextStyle(fontSize: 16)), centerTitle: true), body: Column( @@ -43,24 +44,21 @@ class About extends StatelessWidget { title: const Text("Github"), trailing: const Icon(Icons.arrow_right), onTap: () { - launchUrl(Uri.parse("https://github.com/wanghongenpin/network_proxy_flutter"), - mode: LaunchMode.externalApplication); + launchUrl(Uri.parse(gitHub), mode: LaunchMode.externalApplication); }), ListTile( title: Text(localizations.feedback), trailing: const Icon(Icons.arrow_right), onTap: () { - launchUrl(Uri.parse("https://github.com/wanghongenpin/network_proxy_flutter/issues"), - mode: LaunchMode.externalApplication); + launchUrl(Uri.parse("{$gitHub}/issues"), mode: LaunchMode.externalApplication); }), ListTile( title: Text(isCN ? "下载地址" : "Download"), trailing: const Icon(Icons.arrow_right), onTap: () { launchUrl( - Uri.parse(isCN - ? "https://gitee.com/wanghongenpin/network-proxy-flutter/releases" - : "https://github.com/wanghongenpin/network_proxy_flutter/releases"), + Uri.parse( + isCN ? "https://gitee.com/wanghongenpin/network-proxy-flutter/releases" : "$gitHub/releases"), mode: LaunchMode.externalApplication); }) ], diff --git a/lib/ui/mobile/widgets/remote_device.dart b/lib/ui/mobile/widgets/remote_device.dart index d938d38..9bc668e 100644 --- a/lib/ui/mobile/widgets/remote_device.dart +++ b/lib/ui/mobile/widgets/remote_device.dart @@ -344,7 +344,7 @@ class _RemoteDevicePageState extends State { Navigator.pop(context); } }, - child: Text(localizations.connected)), + child: Text(localizations.connectRemote)), ], ); }); @@ -380,7 +380,12 @@ class _RemoteDevicePageState extends State { } /// + bool doConnecting = false; + + ///连接远程设备 Future doConnect(String host, int port, {bool? ipProxy}) async { + if (doConnecting) return false; + doConnecting = true; try { var response = await HttpClients.get("http://$host:$port/ping", timeout: const Duration(milliseconds: 3000)); if (response.bodyAsString == "pong") { @@ -422,6 +427,8 @@ class _RemoteDevicePageState extends State { }); } return false; + } finally { + doConnecting = false; } } diff --git a/pubspec.yaml b/pubspec.yaml index 7cf717d..8a9c603 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,7 @@ dependencies: flutter_js: ^0.8.1 flutter_code_editor: ^0.3.2 flutter_desktop_context_menu: ^0.2.0 - file_picker: ^8.1.2 + file_picker: ^8.1.3 file_selector: ^1.0.3 win32audio: ^1.3.1 device_info_plus: ^11.1.0