From a8fd8ca30b8b21392934dc9af1360c3486aa15dd Mon Sep 17 00:00:00 2001 From: wanghongenpin Date: Sun, 4 Feb 2024 23:17:53 +0800 Subject: [PATCH] Support blocking request --- lib/l10n/app_en.arb | 7 +- lib/l10n/app_zh.arb | 6 +- .../components/request_block_manager.dart | 118 +++++++ lib/network/handler.dart | 21 +- lib/ui/desktop/desktop.dart | 28 +- .../toolbar/setting/request_block.dart | 228 ++++++++++++++ lib/ui/desktop/toolbar/setting/setting.dart | 20 +- lib/ui/desktop/toolbar/setting/theme.dart | 65 ---- lib/ui/desktop/toolbar/toolbar.dart | 2 +- lib/ui/mobile/menu.dart | 19 +- lib/ui/mobile/mobile.dart | 12 +- lib/ui/mobile/request/request.dart | 288 +++++------------- lib/ui/mobile/setting/filter.dart | 2 +- lib/ui/mobile/setting/request_block.dart | 229 ++++++++++++++ linux/build.sh | 2 +- 15 files changed, 733 insertions(+), 314 deletions(-) create mode 100644 lib/network/components/request_block_manager.dart create mode 100644 lib/ui/desktop/toolbar/setting/request_block.dart delete mode 100644 lib/ui/desktop/toolbar/setting/theme.dart create mode 100644 lib/ui/mobile/setting/request_block.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 33d003a..4ff569e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -181,7 +181,7 @@ "caIosBrowser": "Open Safari on iOS devices:", "localIP": "Local IP ", - "mobileScan": "Scan with Mobile App", + "mobileScan": "Configure Wi-Fi proxy or Scan with Mobile App", "decode": "Decode", "encodeInput": "Enter the content to be converted", @@ -241,6 +241,9 @@ "red" : "Red", "pink" : "Pink", "gray" : "Gray", - "underline" : "Underline" + "underline" : "Underline", + + "requestBlock": "Request Block" + } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 09167a0..b056c76 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -182,7 +182,7 @@ "caIosBrowser": "在 iOS 设备上打开 Safari访问:", "localIP": "本地IP ", - "mobileScan": "请使用手机版扫描二维码", + "mobileScan": "配置Wi-Fi代理或使用手机版扫描二维码", "encodeInput": "输入要转换的内容", "encodeResult": "转换结果", @@ -240,6 +240,8 @@ "red" : "红色", "pink" : "粉色", "gray" : "灰色", - "underline" : "下划线" + "underline" : "下划线", + + "requestBlock": "请求屏蔽" } \ No newline at end of file diff --git a/lib/network/components/request_block_manager.dart b/lib/network/components/request_block_manager.dart new file mode 100644 index 0000000..0d03c25 --- /dev/null +++ b/lib/network/components/request_block_manager.dart @@ -0,0 +1,118 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path_provider/path_provider.dart'; + +/// 请求屏蔽 +/// @author wanghongen +/// 2024/02/02 +class RequestBlockManager { + static RequestBlockManager? _instance; + bool enabled = true; + List list = []; + final File _storageFile; + + RequestBlockManager._(this._storageFile); + + ///单例 + static Future get instance async { + if (_instance == null) { + var file = await configFile(); + _instance = RequestBlockManager._(file); + await _instance?._load(); + } + return _instance!; + } + + static Future configFile() async { + var directory = await getApplicationSupportDirectory().then((it) => it.path); + var file = File('$directory${Platform.pathSeparator}request_block.json'); + if (!await file.exists()) { + await file.create(recursive: true); + } + return file; + } + + ///加载 + Future _load() async { + var json = await _storageFile.readAsString(); + if (json.isEmpty) return; + var config = jsonDecode(json); + enabled = config['enabled'] == true; + list.clear(); + config['list']?.forEach((element) { + list.add(RequestBlockItem.fromJson(element)); + }); + } + + addBlockRequest(RequestBlockItem item) { + list.add(item); + flushConfig(); + } + + removeBlockRequest(int index) { + list.removeAt(index); + flushConfig(); + } + + /// 是否启用 + bool enableBlockRequest(String url) { + if (!enabled) { + return false; + } + return list.any((element) => element.match(url, BlockType.blockRequest)); + } + + bool enableBlockResponse(String url) { + if (!enabled) { + return false; + } + return list.any((element) => element.match(url, BlockType.blockResponse)); + } + + ///刷新配置 + Future flushConfig() async { + _storageFile.writeAsString(jsonEncode({'enabled': enabled, 'list': list})); + } +} + +enum BlockType { + blockRequest('屏蔽请求'), + blockResponse('屏蔽响应'); + + //名称 + final String label; + + const BlockType(this.label); + static BlockType nameOf(String name) { + return BlockType.values.firstWhere((element) => element.name == name); + } +} + +class RequestBlockItem { + bool enabled = true; + String url; + BlockType type; + RegExp? urlReg; + + RequestBlockItem(this.enabled, this.url, this.type); + + //匹配url + bool match(String url, BlockType blockType) { + urlReg ??= RegExp(this.url.replaceAll("*", ".*")); + return enabled && type == blockType && urlReg!.hasMatch(url); + } + + factory RequestBlockItem.fromJson(Map json) { + return RequestBlockItem(json['enabled'], json['url'], BlockType.nameOf(json['type'])); + } + + Map toJson() { + return {'enabled': enabled, 'url': url, 'type': type.name}; + } + + @override + String toString() { + return toJson().toString(); + } +} diff --git a/lib/network/handler.dart b/lib/network/handler.dart index 53b34f1..6d61658 100644 --- a/lib/network/handler.dart +++ b/lib/network/handler.dart @@ -31,6 +31,7 @@ import 'package:network_proxy/network/util/uri.dart'; import 'package:network_proxy/utils/ip.dart'; import 'channel.dart'; +import 'components/request_block_manager.dart'; import 'http_client.dart'; ///请求和响应事件监听 @@ -111,6 +112,7 @@ class HttpProxyChannelHandler extends ChannelHandler { return; } + var uri = '${httpRequest.remoteDomain()}${httpRequest.path()}'; //脚本替换 var scriptManager = await ScriptManager.instance; HttpRequest? request = await scriptManager.runScript(httpRequest); @@ -125,8 +127,16 @@ class HttpProxyChannelHandler extends ChannelHandler { listener?.onRequest(channel, httpRequest); + //屏蔽请求 + var blockRequest = (await RequestBlockManager.instance).enableBlockRequest(uri); + if (blockRequest) { + log.d("[${channel.id}] 屏蔽请求 $uri"); + channel.close(); + remoteChannel.close(); + return; + } + //重定向 - var uri = '${httpRequest.remoteDomain()}${httpRequest.path()}'; String? redirectUrl = await requestRewrites?.getRedirectRule(uri); if (redirectUrl?.isNotEmpty == true) { await redirect(channelContext, channel, httpRequest, redirectUrl!); @@ -243,6 +253,15 @@ class HttpResponseProxyHandler extends ChannelHandler { log.e('[${clientChannel.id}] 响应重写异常 ', error: e, stackTrace: t); } listener?.onResponse(channelContext, msg); + + //屏蔽响应 + var uri = '${request?.remoteDomain()}${request?.path()}'; + var blockResponse = (await RequestBlockManager.instance).enableBlockResponse(uri); + if (blockResponse) { + channel.close(); + return; + } + //发送给客户端 await clientChannel.write(msg); } diff --git a/lib/ui/desktop/desktop.dart b/lib/ui/desktop/desktop.dart index 9dd90fc..6f8611f 100644 --- a/lib/ui/desktop/desktop.dart +++ b/lib/ui/desktop/desktop.dart @@ -130,7 +130,7 @@ class _DesktopHomePagePageState extends State implements EventL BoxDecoration(border: Border(right: BorderSide(color: Theme.of(context).dividerColor, width: 0.2))), child: Column(children: [ SizedBox( - height: 300, + height: 320, child: leftNavigation(index), ), Expanded( @@ -201,21 +201,23 @@ class _DesktopHomePagePageState extends State implements EventL ? '提示:默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n' '点击HTTPS抓包(加锁图标),选择安装根证书,按照提示操作即可。\n\n' '1. 历史记录支持自动缓存时间设置;\n' - '2. 增加当前视图导出;\n' - '3. 历史记录增加搜索;\n' - '4. 支持高亮请求;\n' - '5. Android返回键进入小窗口;\n' - '6. Android白名单应用列表展示隐藏图标应用;\n' - '7. 修复websocket暗黑主题展示不清楚;\n' + '2. 历史记录增加搜索;\n' + '3. 增加当前视图导出;\n' + '4. 增加屏蔽请求功能;\n' + '5. 支持高亮请求;\n' + '6. Android返回键进入小窗口;\n' + '7. Android白名单应用列表展示隐藏图标应用;\n' + '8. 修复websocket暗黑主题展示不清楚;\n' : 'Tips:By default, HTTPS packet capture will not be enabled. Please install the certificate before enabling HTTPS packet capture。\n' 'Click HTTPS Capture packets(Lock icon),Choose to install the root certificate and follow the prompts to proceed。\n\n' '1. History support auto cache time setting;\n' - '2. Add current view export;\n' - '3. History Add Search;\n' - '4. Supports highlighting requests;\n' - '5. Android Return key to enter the small window;\n' - '6. Android Whitelist application list display hidden icon applications;\n' - '7. Fix websocket dark theme display unclear;\n', + '2. History Add Search;\n' + '3. Add current view export;\n' + '4. Support blocking request;\n' + '5. Support highlighting requests;\n' + '6. Android Return key to enter the small window;\n' + '7. Android Whitelist application list display hidden icon applications;\n' + '8. Fix websocket dark theme display unclear;\n', style: const TextStyle(fontSize: 14))); }); } diff --git a/lib/ui/desktop/toolbar/setting/request_block.dart b/lib/ui/desktop/toolbar/setting/request_block.dart new file mode 100644 index 0000000..3bc27e4 --- /dev/null +++ b/lib/ui/desktop/toolbar/setting/request_block.dart @@ -0,0 +1,228 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:network_proxy/network/components/request_block_manager.dart'; +import 'package:network_proxy/ui/component/utils.dart'; +import 'package:network_proxy/ui/component/widgets.dart'; + +class RequestBlock extends StatefulWidget { + final RequestBlockManager requestBlockManager; + + const RequestBlock({super.key, required this.requestBlockManager}); + + @override + State createState() => _RequestBlockState(); +} + +class _RequestBlockState extends State { + AppLocalizations get localizations => AppLocalizations.of(context)!; + bool changed = false; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + if (changed) { + widget.requestBlockManager.flushConfig(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + titlePadding: const EdgeInsets.only(left: 20, top: 10, right: 15), + contentPadding: const EdgeInsets.only(left: 20, right: 20), + scrollable: true, + title: Row(children: [ + Text(localizations.requestBlock, style: const TextStyle(fontSize: 16)), + const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton())) + ]), + content: SizedBox( + width: 550, + height: 500, + child: Column(children: [ + Row(children: [ + const SizedBox(width: 8), + Text(localizations.enable), + const SizedBox(width: 10), + SwitchWidget( + scale: 0.8, + value: widget.requestBlockManager.enabled, + onChanged: (value) { + widget.requestBlockManager.enabled = value; + changed = true; + }), + const Expanded(child: SizedBox()), + FilledButton.icon( + icon: const Icon(Icons.add, size: 14), + onPressed: showEdit, + label: Text(localizations.add, style: const TextStyle(fontSize: 12))), + const SizedBox(width: 5), + ]), + const SizedBox(height: 8), + Container( + height: 430, + decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))), + child: Column(children: [ + const SizedBox(height: 5), + Row( + children: [ + Container(width: 15), + const Expanded(child: Text('URL', style: TextStyle(fontSize: 14))), + SizedBox(width: 80, child: Text(localizations.enable, style: const TextStyle(fontSize: 14))), + Container(width: 18), + SizedBox(width: 120, child: Text(localizations.action, style: const TextStyle(fontSize: 14))), + ], + ), + const Divider(thickness: 0.5), + Expanded( + child: ListView.builder( + itemCount: widget.requestBlockManager.list.length, itemBuilder: (_, index) => row(index))) + ])) + ]))); + } + + Widget row(int index) { + var primaryColor = Theme.of(context).colorScheme.primary; + bool isCN = localizations.localeName == 'zh'; + var list = widget.requestBlockManager.list; + + return InkWell( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + hoverColor: primaryColor.withOpacity(0.3), + onSecondaryTapDown: (details) => showMenus(details, index), + onDoubleTap: () => showEdit(index), + child: Container( + color: index.isEven ? Colors.grey.withOpacity(0.1) : null, + height: 38, + padding: const EdgeInsets.symmetric(vertical: 3), + child: Row( + children: [ + const SizedBox(width: 10), + Expanded(child: Text(list[index].url, style: const TextStyle(fontSize: 14))), + const SizedBox(width: 20), + SwitchWidget( + scale: 0.65, + value: list[index].enabled, + onChanged: (val) { + list[index].enabled = val; + setState(() { + changed = true; + }); + }), + const SizedBox(width: 40), + SizedBox( + width: 130, + child: Text(isCN ? list[index].type.label : list[index].type.name, + style: const TextStyle(fontSize: 14))) + ], + ))); + } + + //点击菜单 + showMenus(TapDownDetails details, int index) { + var list = widget.requestBlockManager.list; + + showContextMenu(context, details.globalPosition, items: [ + PopupMenuItem(height: 35, child: Text(localizations.edit), onTap: () => showEdit(index)), + PopupMenuItem( + height: 35, + child: list[index].enabled ? Text(localizations.disabled) : Text(localizations.enable), + onTap: () { + list[index].enabled = !list[index].enabled; + changed = true; + setState(() {}); + }), + const PopupMenuDivider(), + PopupMenuItem( + height: 35, + child: Text(localizations.delete), + onTap: () async { + await widget.requestBlockManager.removeBlockRequest(index); + setState(() {}); + }) + ]); + } + + showEdit([int? index]) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return RequestBlockAddDialog(requestBlockManager: widget.requestBlockManager, index: index); + }).then((value) { + if (value != null) { + setState(() { + changed = true; + }); + } + }); + } +} + +class RequestBlockAddDialog extends StatelessWidget { + final RequestBlockManager requestBlockManager; + final int? index; + + const RequestBlockAddDialog({super.key, required this.requestBlockManager, this.index}); + + @override + Widget build(BuildContext context) { + AppLocalizations localizations = AppLocalizations.of(context)!; + bool isCN = localizations.localeName == 'zh'; + + GlobalKey formKey = GlobalKey(); + RequestBlockItem item = + index == null ? RequestBlockItem(true, '', BlockType.values.first) : requestBlockManager.list.elementAt(index!); + bool enabled = item.enabled; + return AlertDialog( + scrollable: true, + content: Padding( + padding: const EdgeInsets.all(8.0), + child: Form( + key: formKey, + child: Column(children: [ + SwitchWidget(title: localizations.enable, value: item.enabled, onChanged: (val) => enabled = val), + const SizedBox(height: 20), + TextFormField( + initialValue: item.url, + decoration: const InputDecoration( + labelText: 'URL', hintText: 'https://example.com/*', border: OutlineInputBorder()), + validator: (val) => val == null || val.trim().isEmpty ? localizations.cannotBeEmpty : null, + onSaved: (val) => item.url = val!.trim()), + const SizedBox(height: 20), + DropdownButtonFormField( + value: item.type, + decoration: InputDecoration(labelText: localizations.type, border: const OutlineInputBorder()), + items: BlockType.values + .map((e) => DropdownMenuItem( + value: e, child: Text(isCN ? e.label : e.name, style: const TextStyle(fontSize: 14)))) + .toList(), + onSaved: (val) => item.type = val!, + onChanged: (val) {}), + ]))), + actions: [ + FilledButton( + child: Text(localizations.save), + onPressed: () { + if (!(formKey.currentState as FormState).validate()) { + return; + } + (formKey.currentState as FormState).save(); + + item.enabled = enabled; + if (index != null) { + requestBlockManager.list[index!] = item; + } else { + requestBlockManager.addBlockRequest(item); + } + Navigator.of(context).pop(item); + }), + ElevatedButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop()) + ]); + } +} diff --git a/lib/ui/desktop/toolbar/setting/setting.dart b/lib/ui/desktop/toolbar/setting/setting.dart index dbf99fa..add1365 100644 --- a/lib/ui/desktop/toolbar/setting/setting.dart +++ b/lib/ui/desktop/toolbar/setting/setting.dart @@ -4,12 +4,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; import 'package:network_proxy/network/bin/configuration.dart'; import 'package:network_proxy/network/bin/server.dart'; +import 'package:network_proxy/network/components/request_block_manager.dart'; import 'package:network_proxy/network/util/system_proxy.dart'; import 'package:network_proxy/ui/component/multi_window.dart'; -import 'package:network_proxy/ui/component/utils.dart'; -import 'package:network_proxy/ui/configuration.dart'; import 'package:network_proxy/ui/desktop/toolbar/setting/external_proxy.dart'; -import 'package:network_proxy/ui/desktop/toolbar/setting/theme.dart'; +import 'package:network_proxy/ui/desktop/toolbar/setting/request_block.dart'; import 'package:url_launcher/url_launcher.dart'; import 'filter.dart'; @@ -57,9 +56,9 @@ class _SettingState extends State { }, menuChildren: [ _ProxyMenu(proxyServer: widget.proxyServer), - futureWidget(AppConfiguration.instance, (appConfiguration) => ThemeSetting(appConfiguration: appConfiguration)), item(localizations.domainFilter, onPressed: hostFilter), item(localizations.requestRewrite, onPressed: requestRewrite), + item(localizations.requestBlock, onPressed: showRequestBlock), item(localizations.script, onPressed: () => openScriptWindow()), item(localizations.externalProxy, onPressed: setExternalProxy), item("Github", onPressed: () => launchUrl(Uri.parse("https://github.com/wanghongenpin/network_proxy_flutter"))), @@ -102,6 +101,19 @@ class _SettingState extends State { }, ); } + + //请求屏蔽 + void showRequestBlock() async { + var requestBlockManager = await RequestBlockManager.instance; + if (!mounted) return; + showDialog( + barrierDismissible: false, + context: context, + builder: (context) { + return RequestBlock(requestBlockManager: requestBlockManager); + }, + ); + } } ///代理菜单 diff --git a/lib/ui/desktop/toolbar/setting/theme.dart b/lib/ui/desktop/toolbar/setting/theme.dart deleted file mode 100644 index ae4a550..0000000 --- a/lib/ui/desktop/toolbar/setting/theme.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:network_proxy/ui/configuration.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -/// @author wanghongen -/// 2023/6/17 -class ThemeSetting extends StatelessWidget { - final AppConfiguration appConfiguration; - - const ThemeSetting({super.key, required this.appConfiguration}); - - @override - Widget build(BuildContext context) { - var surfaceTintColor = - Brightness.dark == Theme.of(context).brightness ? null : Theme.of(context).colorScheme.background; - - AppLocalizations localizations = AppLocalizations.of(context)!; - - return SubmenuButton( - menuStyle: MenuStyle( - surfaceTintColor: MaterialStatePropertyAll(surfaceTintColor), - padding: const MaterialStatePropertyAll(EdgeInsets.only(top: 10, bottom: 10)), - ), - menuChildren: [ - SizedBox( - width: 180, - height: 38, - child: Tooltip( - preferBelow: false, - message: localizations.material3, - child: SwitchListTile( - contentPadding: const EdgeInsets.only(left: 32, right: 5), - value: appConfiguration.useMaterial3, - onChanged: (bool value) => appConfiguration.useMaterial3 = value, - dense: true, - title: const Text("Material3"), - ))), - MenuItemButton( - leadingIcon: appConfiguration.themeMode == ThemeMode.system - ? const Icon(Icons.check, size: 15) - : const SizedBox(width: 15), - trailingIcon: const Icon(Icons.cached), - child: Text(localizations.followSystem), - onPressed: () => appConfiguration.themeMode = ThemeMode.system), - MenuItemButton( - leadingIcon: appConfiguration.themeMode == ThemeMode.dark - ? const Icon(Icons.check, size: 15) - : const SizedBox(width: 15), - trailingIcon: const Icon(Icons.nightlight_outlined), - child: Text(localizations.themeDark), - onPressed: () => appConfiguration.themeMode = ThemeMode.dark), - MenuItemButton( - leadingIcon: appConfiguration.themeMode == ThemeMode.light - ? const Icon(Icons.check, size: 15) - : const SizedBox(width: 15), - trailingIcon: const Icon(Icons.sunny), - child: Text(localizations.themeLight), - onPressed: () => appConfiguration.themeMode = ThemeMode.light), - ], - child: Padding( - padding: const EdgeInsets.only(left: 10), - child: Text(localizations.theme, style: const TextStyle(fontSize: 14))), - ); - } -} diff --git a/lib/ui/desktop/toolbar/toolbar.dart b/lib/ui/desktop/toolbar/toolbar.dart index ee8ffb5..b89fbbd 100644 --- a/lib/ui/desktop/toolbar/toolbar.dart +++ b/lib/ui/desktop/toolbar/toolbar.dart @@ -41,8 +41,8 @@ class _ToolbarState extends State { if (event.logicalKey == LogicalKeyboardKey.escape) { if (ModalRoute.of(context)?.isCurrent == false) { Navigator.of(context).pop(); + return true; } - return true; } if (HardwareKeyboard.instance.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyW) { diff --git a/lib/ui/mobile/menu.dart b/lib/ui/mobile/menu.dart index 1c85e4a..2f2f64c 100644 --- a/lib/ui/mobile/menu.dart +++ b/lib/ui/mobile/menu.dart @@ -9,6 +9,7 @@ import 'package:flutter_toastr/flutter_toastr.dart'; import 'package:network_proxy/native/vpn.dart'; import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/network/components/host_filter.dart'; +import 'package:network_proxy/network/components/request_block_manager.dart'; import 'package:network_proxy/network/components/request_rewrite_manager.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/http_client.dart'; @@ -23,6 +24,7 @@ import 'package:network_proxy/ui/mobile/request/history.dart'; import 'package:network_proxy/ui/mobile/setting/app_whitelist.dart'; import 'package:network_proxy/ui/mobile/setting/filter.dart'; import 'package:network_proxy/ui/mobile/setting/proxy.dart'; +import 'package:network_proxy/ui/mobile/setting/request_block.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/setting/ssl.dart'; @@ -89,8 +91,21 @@ class DrawerWidget extends StatelessWidget { ListTile( title: Text(localizations.requestRewrite), leading: const Icon(Icons.replay_outlined), - onTap: () async => - navigator(context, MobileRequestRewrite(requestRewrites: (await RequestRewrites.instance)))), + onTap: () async { + var requestRewrites = await RequestRewrites.instance; + if (context.mounted) { + navigator(context, MobileRequestRewrite(requestRewrites: requestRewrites)); + } + }), + ListTile( + title: Text(localizations.requestBlock), + leading: const Icon(Icons.block_flipped), + onTap: () async { + var requestBlockManager = await RequestBlockManager.instance; + if (context.mounted) { + navigator(context, MobileRequestBlock(requestBlockManager: requestBlockManager)); + } + }), ListTile( title: Text(localizations.script), leading: const Icon(Icons.code), diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index df9471d..3f8cc99 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -226,17 +226,17 @@ class MobileHomeState extends State implements EventListener, Li String content = isCN ? '提示:默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n\n' '1. 历史记录支持自动缓存时间设置;\n' - '2. 增加当前视图导出;\n' - '3. 历史记录增加搜索;\n' - '4. 支持高亮请求;\n' + '2. 历史记录增加搜索;\n' + '3. 增加当前视图导出;\n' + '4. 增加屏蔽请求功能;\n' '5. Android返回键进入小窗口;\n' '6. Android白名单应用列表展示隐藏图标应用;\n' '7. 修复websocket暗黑主题展示不清楚;\n' : 'Tips:By default, HTTPS packet capture will not be enabled. Please install the certificate before enabling HTTPS packet capture。\n\n' '1. History support auto cache time setting;\n' - '2. Add current view export;\n' - '3. History Add Search;\n' - '4. Supports highlighting requests;\n' + '2. History Add Search;\n' + '3. Add current view export;\n' + '4. Support blocking request;\n' '5. Android Return key to enter the small window;\n' '6. Android Whitelist application list display hidden icon applications;\n' '7. Fix websocket dark theme display unclear;\n'; diff --git a/lib/ui/mobile/request/request.dart b/lib/ui/mobile/request/request.dart index ebb5464..515a4b0 100644 --- a/lib/ui/mobile/request/request.dart +++ b/lib/ui/mobile/request/request.dart @@ -40,11 +40,8 @@ class RequestRow extends StatefulWidget { class RequestRowState extends State { late HttpRequest request; HttpResponse? response; - bool selected = false; - Color? highlightColor; //高亮颜色 - AppLocalizations get localizations => AppLocalizations.of(context)!; change(HttpResponse response) { @@ -75,8 +72,6 @@ class RequestRowState extends State { visualDensity: const VisualDensity(vertical: -4), minLeadingWidth: 5, selected: selected, - textColor: highlightColor, - selectedColor: highlightColor, leading: getIcon(response), title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 2, style: const TextStyle(fontSize: 14)), subtitle: Text.rich( @@ -88,7 +83,7 @@ class RequestRowState extends State { trailing: const Icon(Icons.chevron_right, size: 22), dense: true, contentPadding: const EdgeInsets.only(left: 3), - onLongPress: showMenu, + onLongPress: menu, onTap: () { Navigator.push( context, @@ -104,148 +99,76 @@ class RequestRowState extends State { }); } - showMenu() { + ///菜单 + menu() { setState(() { selected = true; }); showModalBottomSheet( - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))), - context: context, - isScrollControlled: true, - enableDrag: true, - builder: (ctx) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Wrap( - children: [ - copyItem(), - const Divider(thickness: 0.3, height: 3), - repeatItem(), - const Divider(thickness: 0.3, height: 5), - ListTile( - dense: true, - title: Text(localizations.highlight, textAlign: TextAlign.center), - trailing: const Icon(Icons.chevron_right), - onTap: () { - //显示高亮菜单 - Navigator.of(context).pop(); - showHighlightMenu(); - }), - ListTile( - dense: true, - title: Text(localizations.favorite, textAlign: TextAlign.center), - trailing: const Icon(Icons.favorite), - onTap: () { - FavoriteStorage.addFavorite(widget.request); - FlutterToastr.show(localizations.addSuccess, context); - Navigator.of(context).pop(); - }, - ), - ListTile( - onTap: () { - widget.onRemove?.call(request); - FlutterToastr.show(localizations.deleteSuccess, context); - Navigator.of(context).pop(); - }, - dense: true, - title: Text(localizations.delete, textAlign: TextAlign.center), - trailing: const Icon(Icons.remove)), - Container( - color: Theme.of(context).hoverColor, - height: 8, - ), - ListTile( - dense: true, - onTap: () => Navigator.of(context).pop(), - title: Container( - height: 50, - width: double.infinity, - padding: const EdgeInsets.only(top: 10), - child: Text(localizations.cancel, textAlign: TextAlign.center)), - trailing: const Icon(Icons.cancel_presentation)), - ], - )); - }).then((value) => setState(() { - selected = false; - })); - } - - Widget copyItem() { - var dividerColor = Theme.of(context).dividerColor; - var styleFrom = OutlinedButton.styleFrom( - textStyle: const TextStyle(fontSize: 14), - side: BorderSide(width: 0.3, color: dividerColor), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))); - - return Wrap(alignment: WrapAlignment.center, children: [ - Container( - padding: const EdgeInsets.only(left: 15, top: 10, bottom: 5), - width: double.infinity, - child: Text(localizations.copy, textAlign: TextAlign.left, style: const TextStyle(fontSize: 12))), - SizedBox( - width: double.infinity, - child: Wrap(alignment: WrapAlignment.spaceAround, spacing: 15, children: [ - OutlinedButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: widget.request.requestUrl)).then((value) { - FlutterToastr.show(localizations.copied, context); - Navigator.of(context).pop(); - }); - }, - style: styleFrom, - child: Text(localizations.copyUrl), - ), - OutlinedButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: curlRequest(widget.request))).then((value) { - FlutterToastr.show(localizations.copied, context); - Navigator.of(context).pop(); - }); - }, - style: styleFrom, - child: Text(localizations.copyCurl)) - ])) - ]); - } - - Widget repeatItem() { - var dividerColor = Theme.of(context).dividerColor; - var styleFrom = OutlinedButton.styleFrom( - textStyle: const TextStyle(fontSize: 14), - side: BorderSide(width: 0.3, color: dividerColor), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))); - - return Wrap(alignment: WrapAlignment.center, children: [ - Container( - padding: const EdgeInsets.only(left: 15, top: 5, bottom: 5), - width: double.infinity, - child: Text(localizations.repeat, textAlign: TextAlign.left, style: const TextStyle(fontSize: 12))), - SizedBox( - width: double.infinity, - child: Wrap(alignment: WrapAlignment.spaceAround, spacing: 15, children: [ - OutlinedButton( + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))), + context: context, + isScrollControlled: true, + enableDrag: true, + builder: (ctx) { + return Wrap(alignment: WrapAlignment.center, children: [ + menuItem(localizations.copyUrl, () => widget.request.requestUrl), + const Divider(thickness: 0.5, height: 5), + menuItem(localizations.copyCurl, () => curlRequest(widget.request)), + const Divider(thickness: 0.5, height: 5), + TextButton( + child: SizedBox(width: double.infinity, child: Text(localizations.repeat, textAlign: TextAlign.center)), onPressed: () { onRepeat(widget.request); Navigator.of(context).pop(); - }, - style: styleFrom, - child: Text(localizations.repeat), - ), - OutlinedButton( - onPressed: () => showCustomRepeat(widget.request), - style: styleFrom, - child: Text(localizations.customRepeat)), - OutlinedButton( - onPressed: () { - Navigator.of(context).pop(); - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => - MobileRequestEditor(request: widget.request, proxyServer: widget.proxyServer))); - }, - style: styleFrom, - child: Text(localizations.editRequest)) - ])) - ]); + }), + const Divider(thickness: 0.5, height: 5), + TextButton( + child: SizedBox( + width: double.infinity, child: Text(localizations.customRepeat, textAlign: TextAlign.center)), + onPressed: () => showCustomRepeat(widget.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: widget.request, proxyServer: widget.proxyServer))); + }), + const Divider(thickness: 0.5, height: 5), + TextButton( + child: SizedBox(width: double.infinity, child: Text(localizations.favorite, textAlign: TextAlign.center)), + onPressed: () { + FavoriteStorage.addFavorite(widget.request); + FlutterToastr.show(localizations.addSuccess, context); + Navigator.of(context).pop(); + }), + const Divider(thickness: 0.5, height: 5), + TextButton( + child: SizedBox(width: double.infinity, child: Text(localizations.delete, textAlign: TextAlign.center)), + onPressed: () { + widget.onRemove?.call(request); + FlutterToastr.show(localizations.deleteSuccess, context); + Navigator.of(context).pop(); + }), + Container( + color: Theme.of(context).hoverColor, + height: 8, + ), + TextButton( + child: Container( + height: 55, + width: double.infinity, + padding: const EdgeInsets.only(top: 10), + child: Text(localizations.cancel, textAlign: TextAlign.center)), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ]); + }, + ).then((value) => setState(() => selected = false)); } //显示高级重发 @@ -265,81 +188,14 @@ class RequestRowState extends State { } } - void showHighlightMenu() { - showModalBottomSheet( - context: context, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))), - enableDrag: true, - builder: (BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Wrap( - children: [ - ListTile( - leading: const Icon(Icons.circle, color: Colors.red), - title: Text(localizations.red), - onTap: () { - setState(() { - highlightColor = Colors.red; - }); - Navigator.of(context).pop(); - }, - ), - ListTile( - leading: const Icon(Icons.circle, color: Colors.yellow), - title: Text(localizations.yellow), - onTap: () { - setState(() { - highlightColor = Colors.yellow.shade600; - }); - Navigator.of(context).pop(); - }, - ), - ListTile( - leading: const Icon(Icons.circle, color: Colors.blue), - title: Text(localizations.blue), - onTap: () { - setState(() { - highlightColor = Colors.blue; - }); - Navigator.of(context).pop(); - }, - ), - ListTile( - leading: const Icon(Icons.circle, color: Colors.green), - title: Text(localizations.green), - onTap: () { - setState(() { - highlightColor = Colors.green; - }); - Navigator.of(context).pop(); - }, - ), - ListTile( - leading: const Icon(Icons.circle, color: Colors.grey), - title: Text(localizations.gray), - onTap: () { - setState(() { - highlightColor = Colors.grey; - }); - Navigator.of(context).pop(); - }, - ), - const Divider(), - ListTile( - leading: const Icon(Icons.restart_alt), - title: Text(localizations.reset), - onTap: () { - setState(() { - highlightColor = null; - }); - Navigator.of(context).pop(); - }, - ), - const SizedBox(height: 10) - ], - ), - ); + 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(); + }); }); } } diff --git a/lib/ui/mobile/setting/filter.dart b/lib/ui/mobile/setting/filter.dart index dad949d..745847b 100644 --- a/lib/ui/mobile/setting/filter.dart +++ b/lib/ui/mobile/setting/filter.dart @@ -182,7 +182,7 @@ class DomainAddDialog extends StatelessWidget { ]))), actions: [ FilledButton( - child: Text(localizations.add), + child: Text(localizations.save), onPressed: () { if (!(formKey.currentState as FormState).validate()) { return; diff --git a/lib/ui/mobile/setting/request_block.dart b/lib/ui/mobile/setting/request_block.dart new file mode 100644 index 0000000..240ecbe --- /dev/null +++ b/lib/ui/mobile/setting/request_block.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:network_proxy/network/components/request_block_manager.dart'; +import 'package:network_proxy/ui/component/widgets.dart'; +import 'package:network_proxy/utils/lang.dart'; + +class MobileRequestBlock extends StatefulWidget { + final RequestBlockManager requestBlockManager; + + const MobileRequestBlock({super.key, required this.requestBlockManager}); + + @override + State createState() => _RequestBlockState(); +} + +class _RequestBlockState extends State { + AppLocalizations get localizations => AppLocalizations.of(context)!; + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar(title: Text(localizations.requestBlock, style: const TextStyle(fontSize: 16))), + body: Container( + padding: const EdgeInsets.all(10), + child: Column(children: [ + Row(children: [ + const SizedBox(width: 8), + Text(localizations.enable), + const SizedBox(width: 10), + SwitchWidget( + scale: 0.8, + value: widget.requestBlockManager.enabled, + onChanged: (value) { + widget.requestBlockManager.enabled = value; + widget.requestBlockManager.flushConfig(); + }), + const Expanded(child: SizedBox()), + FilledButton.icon( + icon: const Icon(Icons.add, size: 14), + onPressed: showEdit, + label: Text(localizations.add, style: const TextStyle(fontSize: 12))), + const SizedBox(width: 5), + ]), + const SizedBox(height: 8), + Container( + height: 620, + decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))), + child: Column(children: [ + const SizedBox(height: 5), + Row( + children: [ + Container(width: 15), + const Expanded(child: Text('URL', style: TextStyle(fontSize: 14))), + SizedBox(width: 60, child: Text(localizations.enable, style: const TextStyle(fontSize: 14))), + SizedBox(width: 75, child: Text(localizations.action, style: const TextStyle(fontSize: 14))), + ], + ), + const Divider(thickness: 0.5), + Expanded( + child: ListView.builder( + itemCount: widget.requestBlockManager.list.length, itemBuilder: (_, index) => row(index))) + ])) + ]))); + } + + Widget row(int index) { + var primaryColor = Theme.of(context).colorScheme.primary; + bool isCN = localizations.localeName == 'zh'; + var list = widget.requestBlockManager.list; + + return InkWell( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + hoverColor: primaryColor.withOpacity(0.3), + onLongPress: () => showMenus(index), + onTap: () => showEdit(index), + child: Container( + color: index.isEven ? Colors.grey.withOpacity(0.1) : null, + height: 38, + padding: const EdgeInsets.symmetric(vertical: 3), + child: Row( + children: [ + const SizedBox(width: 10), + Expanded(child: Text(list[index].url.fixAutoLines(), style: const TextStyle(fontSize: 13))), + const SizedBox(width: 5), + SwitchWidget( + scale: 0.65, + value: list[index].enabled, + onChanged: (val) { + list[index].enabled = val; + setState(() { + widget.requestBlockManager.flushConfig(); + }); + }), + const SizedBox(width: 5), + SizedBox( + width: 85, + child: Text(isCN ? list[index].type.label : list[index].type.name, + style: const TextStyle(fontSize: 13))) + ], + ))); + } + + //点击菜单 + showMenus(int index) { + var list = widget.requestBlockManager.list; + + showModalBottomSheet( + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))), + context: context, + isScrollControlled: true, + enableDrag: true, + builder: (ctx) { + return Wrap(children: [ + BottomSheetItem(text: localizations.edit, onPressed: () => showEdit(index)), + const Divider(thickness: 0.5, height: 5), + BottomSheetItem( + text: list[index].enabled ? localizations.disabled : localizations.enable, + onPressed: () { + list[index].enabled = !list[index].enabled; + setState(() { + widget.requestBlockManager.flushConfig(); + }); + }), + const Divider(thickness: 0.5, height: 5), + BottomSheetItem( + text: localizations.delete, + onPressed: () async { + await widget.requestBlockManager.removeBlockRequest(index); + setState(() {}); + }), + Container(color: Theme.of(context).hoverColor, height: 8), + TextButton( + child: Container( + height: 50, + width: double.infinity, + padding: const EdgeInsets.only(top: 10), + child: Text(localizations.cancel, textAlign: TextAlign.center)), + onPressed: () { + Navigator.of(context).pop(); + }), + ]); + }); + } + + showEdit([int? index]) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return RequestBlockAddDialog(requestBlockManager: widget.requestBlockManager, index: index); + }).then((value) { + if (value != null) { + setState(() {}); + } + }); + } +} + +class RequestBlockAddDialog extends StatelessWidget { + final RequestBlockManager requestBlockManager; + final int? index; + + const RequestBlockAddDialog({super.key, required this.requestBlockManager, this.index}); + + @override + Widget build(BuildContext context) { + AppLocalizations localizations = AppLocalizations.of(context)!; + bool isCN = localizations.localeName == 'zh'; + + GlobalKey formKey = GlobalKey(); + RequestBlockItem item = + index == null ? RequestBlockItem(true, '', BlockType.values.first) : requestBlockManager.list.elementAt(index!); + bool enabled = item.enabled; + return AlertDialog( + scrollable: true, + content: Padding( + padding: const EdgeInsets.all(8.0), + child: Form( + key: formKey, + child: Column(children: [ + SwitchWidget(title: localizations.enable, value: item.enabled, onChanged: (val) => enabled = val), + const SizedBox(height: 10), + TextFormField( + initialValue: item.url.fixAutoLines(), + maxLines: 3, + minLines: 1, + decoration: const InputDecoration( + isDense: true, + labelText: 'URL', + hintText: 'https://example.com/*', + border: OutlineInputBorder()), + validator: (val) => val == null || val.trim().isEmpty ? localizations.cannotBeEmpty : null, + onSaved: (val) => item.url = val!.trim()), + const SizedBox(height: 15), + DropdownButtonFormField( + value: item.type, + decoration: InputDecoration( + isDense: true, labelText: localizations.type, border: const OutlineInputBorder()), + items: BlockType.values + .map((e) => DropdownMenuItem( + value: e, child: Text(isCN ? e.label : e.name, style: const TextStyle(fontSize: 14)))) + .toList(), + onSaved: (val) => item.type = val!, + onChanged: (val) {}), + ]))), + actions: [ + FilledButton( + child: Text(localizations.save), + onPressed: () { + if (!(formKey.currentState as FormState).validate()) { + return; + } + (formKey.currentState as FormState).save(); + + item.enabled = enabled; + if (index != null) { + requestBlockManager.list[index!] = item; + } else { + requestBlockManager.addBlockRequest(item); + } + requestBlockManager.flushConfig(); + Navigator.of(context).pop(item); + }), + ElevatedButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop()) + ]); + } +} diff --git a/linux/build.sh b/linux/build.sh index c02685d..cb0ec00 100644 --- a/linux/build.sh +++ b/linux/build.sh @@ -4,7 +4,7 @@ cd ../build/linux/x64/release rm -rf package mkdir -p package/DEBIAN echo "Package: ProxyPin" >> package/DEBIAN/control -echo "Version: 1.0.7" >> package/DEBIAN/control +echo "Version: 1.0.8" >> package/DEBIAN/control echo "Priority: optional" >> package/DEBIAN/control echo "Architecture: amd64" >> package/DEBIAN/control echo "Depends: ca-certificates" >> package/DEBIAN/control