From 4f05f29e25f620d8e7883e34e541384a228da697 Mon Sep 17 00:00:00 2001 From: wanghongenpin Date: Mon, 1 Jan 2024 05:43:30 +0800 Subject: [PATCH] =?UTF-8?q?headers=E9=BB=98=E8=AE=A4=E5=B1=95=E5=BC=80?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/l10n/app_en.arb | 40 ++++-- lib/l10n/app_zh.arb | 43 +++++-- lib/main.dart | 34 +++-- lib/network/bin/configuration.dart | 10 -- lib/network/network.dart | 4 +- lib/network/util/byte_buf.dart | 2 +- lib/network/util/x509.dart | 2 +- lib/ui/configuration.dart | 47 ++++--- lib/ui/content/panel.dart | 3 +- lib/ui/desktop/desktop.dart | 117 +++++++++++++++--- lib/ui/desktop/left/list.dart | 10 +- lib/ui/desktop/left/search.dart | 16 +-- .../toolbar/setting/external_proxy.dart | 18 +-- lib/ui/desktop/toolbar/setting/setting.dart | 22 ++-- lib/ui/desktop/toolbar/setting/theme.dart | 20 +-- lib/ui/desktop/toolbar/ssl/ssl.dart | 62 +++++----- lib/ui/mobile/menu.dart | 31 +++-- lib/ui/mobile/mobile.dart | 67 +++++----- lib/ui/mobile/request/favorite.dart | 2 +- lib/ui/mobile/request/list.dart | 6 +- lib/ui/mobile/setting/proxy.dart | 19 +-- lib/ui/mobile/setting/ssl.dart | 34 ++--- lib/ui/mobile/setting/theme.dart | 18 +-- test/widget_test.dart | 4 +- 24 files changed, 394 insertions(+), 237 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a67f151..0de923b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -9,7 +9,11 @@ "filter": "Filter", "script": "Script", "share": "Share", + "port": "Port: ", "proxy": "Proxy", + "externalProxy": "External proxy", + "proxySetting": "Proxy Setting", + "systemProxy": "Set as System Proxy", "start": "Start", "stop": "Stop", @@ -55,15 +59,16 @@ "statusCode": "Status code: ", "sequence": "All Requests", - "domainList":"Domain List", + "domainList": "Domain List", "domainWhitelist": "Domain Whitelist", "domainBlacklist": "Domain Blacklist", + "domainFilter": "Domain Filter", "appWhitelist": "App Whitelist", "scanCode": "Scan Code Connect", - "addBlacklist":"Add Filter Blacklist", - "addWhitelist":"Add Filter Whitelist", - "deleteWhitelist":"Delete Filter Whitelist", - "domainListSubtitle":"Last Request Time: {time}, Count: {count}", + "addBlacklist": "Add Filter Blacklist", + "addWhitelist": "Add Filter Whitelist", + "deleteWhitelist": "Delete Filter Whitelist", + "domainListSubtitle": "Last Request Time: {time}, Count: {count}", "copy": "Copy", "copyUrl": "Copy URL", @@ -73,12 +78,12 @@ "rename": "Rename", "repeat": "Repeat Request", "editRequest": "Edit and Request", - "reSendRequest" :"The request has been resent", + "reSendRequest": "The request has been resent", "favorite": "Favorite", "deleteFavorite": "Delete Favorite", - "emptyFavorite":"Empty Favorite", - "deleteFavoriteSuccess":"Favorite deleted", + "emptyFavorite": "Empty Favorite", + "deleteFavoriteSuccess": "Favorite deleted", "name": "Name", "historyRecord": "History", @@ -96,6 +101,20 @@ "httpRequest": "HTTP Request", "enabledHttps": "Enable HTTPS Proxy", + "installRootCa": "Install Certificate", + "installCaLocal": "Install Certificate to This PC", + "downloadRootCa": "Download Certificate", + "trustCa": "Trust Certificate", + "profileDownload": "Profile Download", + "install": "Install", + "installCaDescribe": "Install CA Setting > Profile Download > Install", + "trustCaDescribe": "Trust CA Setting > General > About > Certificate Trust Setting", + "androidRoot": "ROOT User: Install to system certificate", + "androidRootMagisk": "For Android ROOT users, a Magisk module ProxyPinCA system certificate has been made, restart the phone after installation.", + "androidRootRename": "If the module does not take effect, you can install the system root certificate according to the online tutorial, and name the root certificate 243f0bfb.0", + "androidUserCA": "Non-ROOT User: Install to user certificate (many apps will not trust user certificates)", + "androidUserCAInstall": "Open settings -> Security -> Encryption and credentials -> Install certificate -> CA certificate", + "androidUserXposed": "It is recommended to use the Xposed module for packet capture (no need for ROOT), click to view wiki", "localIP": "Local IP ", "mobileScan": "Scan with Mobile App", @@ -115,7 +134,8 @@ "curlSchemeRequest": "If the curl format is recognized, should it be converted into an HTTP request?", "windowMode": "Window Mode", - "windowModeSubTitle": "Enabled Packet Capture, Enter the background, Display a small window" - + "windowModeSubTitle": "Enabled Packet Capture, Enter the background, Display a small window", + "headerExpanded": "Headers Expanded", + "headerExpandedSubtitle": "Details page Headers column is expanded by default" } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 4e83f42..692741a 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -9,7 +9,11 @@ "filter": "过滤", "script": "脚本", "share": "分享", + "port": "端口号: ", "proxy": "代理", + "externalProxy": "外部代理设置", + "proxySetting": "代理设置", + "systemProxy": "设置为系统代理", "start": "开始", "stop": "停止", @@ -54,15 +58,16 @@ "statusCode": "状态码:", "sequence": "全部请求", - "domainList":"域名列表", + "domainList": "域名列表", "domainWhitelist": "域名白名单", "domainBlacklist" :"域名黑名单", "appWhitelist":"应用白名单", + "domainFilter":"域名过滤", "scanCode": "扫码连接", - "addBlacklist":"添加黑名单", - "addWhitelist":"添加白名单", - "deleteWhitelist":"删除白名单", - "domainListSubtitle":"最后请求时间: {time}, 次数: {count}", + "addBlacklist": "添加黑名单", + "addWhitelist": "添加白名单", + "deleteWhitelist": "删除白名单", + "domainListSubtitle": "最后请求时间: {time}, 次数: {count}", "copy": "复制", "copyUrl": "复制请求连接", @@ -72,12 +77,12 @@ "rename": "重命名", "repeat": "重放请求", "editRequest": "编辑请求重放", - "reSendRequest" :"已重新发送请求", + "reSendRequest": "已重新发送请求", "favorite": "收藏请求", - "deleteFavorite":"删除收藏", - "emptyFavorite":"暂无收藏", - "deleteFavoriteSuccess":"已删除收藏", + "deleteFavorite": "删除收藏", + "emptyFavorite": "暂无收藏", + "deleteFavoriteSuccess": "已删除收藏", "name": "名称", "historyRecord": "历史记录", @@ -95,6 +100,21 @@ "httpRequest": "HTTP请求", "enabledHttps": "启用HTTPS代理", + "installRootCa": "安装根证书", + "installCaLocal": "安装根证书到本机", + "downloadRootCa": "下载根证书", + "trustCa": "信任证书", + "profileDownload": "已下载描述文件", + "install": "安装", + "installCaDescribe": "安装证书 设置 > 已下载描述文件 > 安装", + "trustCaDescribe": "信任证书 设置 > 通用 > 关于本机 > 证书信任设置", + "androidRoot": "ROOT用户: 安装到系统证书", + "androidRootMagisk": "针对安卓ROOT用户做了个Magisk模块ProxyPinCA系统证书,安装完重启手机即可。", + "androidRootRename": "模块不生效可以根据网上教程安装系统根证书, 根证书命名成 243f0bfb.0", + "androidUserCA": "非ROOT用户: 安装到用户证书(很多软件不会信任用户证书)", + "androidUserCAInstall": "打开设置 -> 安全 -> 加密和凭据 -> 安装证书 -> CA 证书", + "androidUserXposed": "推荐使用Xposed模块抓包(无需ROOT), 点击查看wiki", + "localIP": "本地IP ", "mobileScan": "请使用手机版扫描二维码", @@ -114,6 +134,7 @@ "curlSchemeRequest": "识别到curl格式,是否转换为HTTP请求?", "windowMode": "窗口模式", - "windowModeSubTitle": "开启抓包后 如果应用退回到后台,显示一个小窗口" - + "windowModeSubTitle": "开启抓包后 如果应用退回到后台,显示一个小窗口", + "headerExpanded": "Headers默认展开", + "headerExpandedSubtitle": "详情页Headers栏是否默认展开" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 588b546..e9ee7ae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,33 +2,33 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:network_proxy/network/bin/configuration.dart'; import 'package:network_proxy/ui/component/chinese_font.dart'; import 'package:network_proxy/ui/component/multi_window.dart'; +import 'package:network_proxy/ui/configuration.dart'; import 'package:network_proxy/ui/desktop/desktop.dart'; import 'package:network_proxy/ui/mobile/mobile.dart'; -import 'package:network_proxy/ui/configuration.dart'; import 'package:network_proxy/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; void main(List args) async { WidgetsFlutterBinding.ensureInitialized(); - var instance = UIConfiguration.instance; + var instance = AppConfiguration.instance; //多窗口 if (args.firstOrNull == 'multi_window') { final windowId = int.parse(args[1]); final argument = args[2].isEmpty ? const {} : jsonDecode(args[2]) as Map; - runApp(FluentApp(multiWindow(windowId, argument), uiConfiguration: (await instance))); + runApp(FluentApp(multiWindow(windowId, argument), (await instance))); return; } var configuration = Configuration.instance; //移动端 if (Platforms.isMobile()) { - var uiConfiguration = await instance; - runApp(FluentApp(MobileHomePage(configuration: (await configuration)), uiConfiguration: uiConfiguration)); + var appConfiguration = await instance; + runApp(FluentApp(MobileHomePage((await configuration), appConfiguration), appConfiguration)); return; } @@ -45,20 +45,16 @@ void main(List args) async { await windowManager.focus(); }); - var uiConfiguration = await instance; + var appConfiguration = await instance; registerMethodHandler(); - runApp(FluentApp(DesktopHomePage(configuration: await configuration), uiConfiguration: uiConfiguration)); + runApp(FluentApp(DesktopHomePage(await configuration, appConfiguration), appConfiguration)); } class FluentApp extends StatelessWidget { final Widget home; - final UIConfiguration uiConfiguration; + final AppConfiguration appConfiguration; - const FluentApp( - this.home, { - super.key, - required this.uiConfiguration, - }); + const FluentApp(this.home, this.appConfiguration, {super.key}); @override Widget build(BuildContext context) { @@ -76,15 +72,15 @@ class FluentApp extends StatelessWidget { } return ValueListenableBuilder( - valueListenable: uiConfiguration.globalChange, + valueListenable: appConfiguration.globalChange, builder: (_, current, __) { return MaterialApp( title: 'ProxyPin', debugShowCheckedModeBanner: false, - theme: uiConfiguration.useMaterial3 ? material3Light : light, - darkTheme: uiConfiguration.useMaterial3 ? material3Dark : darkTheme, - themeMode: uiConfiguration.themeMode, - locale: uiConfiguration.language, + theme: appConfiguration.useMaterial3 ? material3Light : light, + darkTheme: appConfiguration.useMaterial3 ? material3Dark : darkTheme, + themeMode: appConfiguration.themeMode, + locale: appConfiguration.language, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: home, diff --git a/lib/network/bin/configuration.dart b/lib/network/bin/configuration.dart index 5f373e9..af6c48f 100644 --- a/lib/network/bin/configuration.dart +++ b/lib/network/bin/configuration.dart @@ -36,18 +36,12 @@ class Configuration { //代理忽略域名 String proxyPassDomains = SystemProxy.proxyPassDomains; - //是否显示更新内容公告 - bool upgradeNoticeV6 = true; - //外部代理 ProxyInfo? externalProxy; //白名单应用 List appWhitelist = []; - /// 是否启用小窗口 - bool smallWindow = false; - //远程连接 不持久化保存 String? remoteHost; @@ -77,14 +71,12 @@ class Configuration { enableSsl = config['enableSsl'] == true; enableSystemProxy = config['enableSystemProxy'] ?? (config['enableDesktop'] ?? true); proxyPassDomains = config['proxyPassDomains'] ?? SystemProxy.proxyPassDomains; - upgradeNoticeV6 = config['upgradeNoticeV6'] ?? true; if (config['externalProxy'] != null) { externalProxy = ProxyInfo.fromJson(config['externalProxy']); } appWhitelist = List.from(config['appWhitelist'] ?? []); HostFilter.whitelist.load(config['whitelist']); HostFilter.blacklist.load(config['blacklist']); - smallWindow = config['smallWindow'] ?? Platform.isAndroid; } /// 配置文件 @@ -123,7 +115,6 @@ class Configuration { Map toJson() { return { - 'upgradeNoticeV6': upgradeNoticeV6, 'port': port, 'enableSsl': enableSsl, 'enableSystemProxy': enableSystemProxy, @@ -132,7 +123,6 @@ class Configuration { 'appWhitelist': appWhitelist, 'whitelist': HostFilter.whitelist.toJson(), 'blacklist': HostFilter.blacklist.toJson(), - 'smallWindow': smallWindow, }; } } diff --git a/lib/network/network.dart b/lib/network/network.dart index 31a715c..16edb9f 100644 --- a/lib/network/network.dart +++ b/lib/network/network.dart @@ -25,7 +25,6 @@ import 'package:network_proxy/network/handler.dart'; import 'package:network_proxy/network/util/attribute_keys.dart'; import 'package:network_proxy/network/util/crts.dart'; import 'package:network_proxy/network/util/tls.dart'; -import 'package:network_proxy/utils/platform.dart'; import 'host_port.dart'; @@ -108,8 +107,7 @@ class Server extends Network { HostAndPort? hostAndPort = channelContext.host; //黑名单 或 没开启https 直接转发 - if ((Platforms.isMobile() && HostFilter.filter(hostAndPort?.host)) || - (hostAndPort?.isSsl() == true && configuration.enableSsl == false)) { + if ((HostFilter.filter(hostAndPort?.host)) || (hostAndPort?.isSsl() == true && configuration.enableSsl == false)) { var remoteChannel = channelContext.serverChannel ?? await channelContext.connectServerChannel(hostAndPort!, RelayHandler(channel)); relay(channel, remoteChannel); diff --git a/lib/network/util/byte_buf.dart b/lib/network/util/byte_buf.dart index 4730447..dac9dc7 100644 --- a/lib/network/util/byte_buf.dart +++ b/lib/network/util/byte_buf.dart @@ -2,7 +2,7 @@ import 'dart:typed_data'; ///类似于netty ByteBuf class ByteBuf { - BytesBuilder _buffer = BytesBuilder(); + final BytesBuilder _buffer = BytesBuilder(); int readerIndex = 0; Uint8List get bytes => _buffer.toBytes(); diff --git a/lib/network/util/x509.dart b/lib/network/util/x509.dart index 38b079a..841c73a 100644 --- a/lib/network/util/x509.dart +++ b/lib/network/util/x509.dart @@ -1,4 +1,4 @@ -// ignore_for_file: constant_identifier_names +// ignore_for_file: constant_identifier_names, depend_on_referenced_packages import 'dart:convert'; import 'dart:typed_data'; diff --git a/lib/ui/configuration.dart b/lib/ui/configuration.dart index a6c4b44..c0c9e96 100644 --- a/lib/ui/configuration.dart +++ b/lib/ui/configuration.dart @@ -2,50 +2,53 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:network_proxy/ui/mobile/connect_remote.dart'; import 'package:network_proxy/utils/platform.dart'; import 'package:path_provider/path_provider.dart'; -class AppConfiguration { - ///画中画 - static final ValueNotifier pictureInPictureNotifier = ValueNotifier(false); - - /// 远程连接 - static final ValueNotifier desktop = ValueNotifier(RemoteModel(connect: false)); -} - class ThemeModel { ThemeMode mode; bool useMaterial3; ThemeModel({this.mode = ThemeMode.system, this.useMaterial3 = true}); - ThemeModel copy({ThemeMode? mode, bool? useMaterial3}) => ThemeModel( + ThemeModel copy({ThemeMode? mode, bool? useMaterial3}) => + ThemeModel( mode: mode ?? this.mode, useMaterial3: useMaterial3 ?? this.useMaterial3, ); } -class UIConfiguration { +class AppConfiguration { ValueNotifier globalChange = ValueNotifier(false); ThemeModel _theme = ThemeModel(); Locale? _language; - UIConfiguration._(); + //是否显示更新内容公告 + bool upgradeNoticeV7 = true; + + /// 是否启用小窗口 + bool smallWindow = false; + + /// + bool headerExpanded = true; + + AppConfiguration._(); /// 单例 - static UIConfiguration? _instance; + static AppConfiguration? _instance; - static Future get instance async { + static Future get instance async { if (_instance == null) { - UIConfiguration configuration = UIConfiguration._(); + AppConfiguration configuration = AppConfiguration._(); await configuration.initConfig(); _instance = configuration; } return _instance!; } + static AppConfiguration? get current => _instance; + ThemeMode get themeMode => _theme.mode; set themeMode(ThemeMode mode) { @@ -106,9 +109,12 @@ class UIConfiguration { try { Map config = jsonDecode(json); var mode = - ThemeMode.values.firstWhere((element) => element.name == config['mode'], orElse: () => ThemeMode.system); + ThemeMode.values.firstWhere((element) => element.name == config['mode'], orElse: () => ThemeMode.system); _theme = ThemeModel(mode: mode, useMaterial3: config['useMaterial3'] ?? true); + upgradeNoticeV7 = config['upgradeNoticeV7'] ?? true; _language = config['language'] == null ? null : Locale.fromSubtags(languageCode: config['language']); + smallWindow = config['smallWindow'] ?? Platform.isAndroid; + headerExpanded = config['headerExpanded'] ?? true; } catch (e) { print(e); } @@ -127,6 +133,13 @@ class UIConfiguration { } Map toJson() { - return {'mode': _theme.mode.name, 'useMaterial3': _theme.useMaterial3, "language": _language?.languageCode}; + return { + 'mode': _theme.mode.name, + 'useMaterial3': _theme.useMaterial3, + 'upgradeNoticeV7': upgradeNoticeV7, + "language": _language?.languageCode, + 'smallWindow': smallWindow, + "headerExpanded": headerExpanded + }; } } diff --git a/lib/ui/content/panel.dart b/lib/ui/content/panel.dart index c54fa0f..91d7560 100644 --- a/lib/ui/content/panel.dart +++ b/lib/ui/content/panel.dart @@ -5,6 +5,7 @@ import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/http/websocket.dart'; import 'package:network_proxy/ui/component/share.dart'; import 'package:network_proxy/ui/component/utils.dart'; +import 'package:network_proxy/ui/configuration.dart'; import 'package:network_proxy/utils/lang.dart'; import 'package:network_proxy/utils/platform.dart'; @@ -270,7 +271,7 @@ class NetworkTabState extends State with SingleTickerProvi Widget headerWidget = ExpansionTile( tilePadding: const EdgeInsets.only(left: 0), title: Text("$type Headers", style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14)), - initiallyExpanded: true, + initiallyExpanded: AppConfiguration.current?.headerExpanded ?? true, shape: const Border(), children: headers); diff --git a/lib/ui/desktop/desktop.dart b/lib/ui/desktop/desktop.dart index 6a6ef2b..ceb505c 100644 --- a/lib/ui/desktop/desktop.dart +++ b/lib/ui/desktop/desktop.dart @@ -8,6 +8,8 @@ import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/http/websocket.dart'; import 'package:network_proxy/ui/component/state_component.dart'; import 'package:network_proxy/ui/component/toolbox.dart'; +import 'package:network_proxy/ui/component/widgets.dart'; +import 'package:network_proxy/ui/configuration.dart'; import 'package:network_proxy/ui/content/panel.dart'; import 'package:network_proxy/ui/desktop/left/favorite.dart'; import 'package:network_proxy/ui/desktop/left/history.dart'; @@ -19,8 +21,9 @@ import '../component/split_view.dart'; class DesktopHomePage extends StatefulWidget { final Configuration configuration; + final AppConfiguration appConfiguration; - const DesktopHomePage({super.key, required this.configuration}); + const DesktopHomePage(this.configuration, this.appConfiguration, {super.key, required}); @override State createState() => _DesktopHomePagePageState(); @@ -77,7 +80,7 @@ class _DesktopHomePagePageState extends State implements EventL proxyServer.addListener(this); panel = NetworkTabController(tabStyle: const TextStyle(fontSize: 16), proxyServer: proxyServer); - if (widget.configuration.upgradeNoticeV6) { + if (widget.appConfiguration.upgradeNoticeV7) { WidgetsBinding.instance.addPostFrameCallback((_) { showUpgradeNotice(); }); @@ -132,12 +135,14 @@ class _DesktopHomePagePageState extends State implements EventL child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ - //偏好设置 Tooltip( message: localizations.preference, preferBelow: false, child: IconButton( - onPressed: () {}, icon: Icon(Icons.settings_outlined, color: Colors.grey.shade500))), + onPressed: () { + showDialog(context: context, builder: (_) => Preference(widget.appConfiguration)); + }, + icon: Icon(Icons.settings_outlined, color: Colors.grey.shade500))), const SizedBox(height: 5), Tooltip( preferBelow: true, @@ -171,6 +176,8 @@ class _DesktopHomePagePageState extends State implements EventL //更新引导 showUpgradeNotice() { + bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh'); + showDialog( context: context, barrierDismissible: false, @@ -180,24 +187,96 @@ class _DesktopHomePagePageState extends State implements EventL actions: [ TextButton( onPressed: () { - widget.configuration.upgradeNoticeV6 = false; - widget.configuration.flushConfig(); + widget.appConfiguration.upgradeNoticeV7 = false; + widget.appConfiguration.flushConfig(); Navigator.pop(context); }, - child: const Text('关闭')) + child: Text(localizations.cancel)) ], - title: const Text('更新内容V1.0.6', style: TextStyle(fontSize: 18)), - content: const Text( - '提示:默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n' - '点击的HTTPS抓包(加锁图标),选择安装根证书,按照提示操作即可。\n\n' - '1. 请求重写增加 修改请求,可根据正则替换;\n' - '2. 请求重写批量导入、导出;\n' - '3. 支持WebSocket抓包;\n' - '4. 安卓支持小窗口模式;\n' - '5. 优化curl导入;\n' - '6. 支持head请求,修复手机端请求重写切换应用恢复原始的请求问题;\n' - '', - style: TextStyle(fontSize: 14))); + title: Text(isCN ? '更新内容V1.0.6' : "Update content V1.0.6", style: const TextStyle(fontSize: 18)), + content: Text( + isCN + ? '提示:默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n' + '点击HTTPS抓包(加锁图标),选择安装根证书,按照提示操作即可。\n\n' + '1. 请求重写增加 修改请求,可根据正则替换;\n' + '2. 请求重写批量导入、导出;\n' + '3. 支持WebSocket抓包;\n' + '4. 安卓支持小窗口模式;\n' + '5. 优化curl导入;\n' + '6. 支持head请求,修复手机端请求重写切换应用恢复原始的请求问题;' + : '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. Increase multilingual support;\n' + '2. Details page Headers Expanded Config;\n', + style: const TextStyle(fontSize: 14))); }); } } + +class Preference extends StatelessWidget { + final AppConfiguration appConfiguration; + + const Preference(this.appConfiguration, {super.key}); + + @override + Widget build(BuildContext context) { + AppLocalizations localizations = AppLocalizations.of(context)!; + var titleMedium = Theme.of(context).textTheme.titleMedium; + return AlertDialog( + scrollable: true, + title: Row(children: [ + const Icon(Icons.settings, size: 20), + const SizedBox(width: 10), + Text(localizations.preference, style: Theme.of(context).textTheme.titleMedium), + const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton())) + ]), + content: SizedBox( + width: 280, + child: Column(children: [ + Row(children: [ + SizedBox(width: 100, child: Text("${localizations.language}: ", style: titleMedium)), + DropdownButton( + value: appConfiguration.language, + onChanged: (Locale? value) => appConfiguration.language = value, + focusColor: Colors.transparent, + items: [ + DropdownMenuItem(value: null, child: Text(localizations.followSystem)), + const DropdownMenuItem(value: Locale.fromSubtags(languageCode: "zh"), child: Text("中文")), + const DropdownMenuItem(value: Locale.fromSubtags(languageCode: "en"), child: Text("English")), + ]), + ]), + //主题 + Row(children: [ + SizedBox(width: 100, child: Text("${localizations.theme}: ", style: titleMedium)), + DropdownButton( + value: appConfiguration.themeMode, + onChanged: (ThemeMode? value) => appConfiguration.themeMode = value!, + focusColor: Colors.transparent, + items: [ + DropdownMenuItem(value: ThemeMode.system, child: Text(localizations.followSystem)), + DropdownMenuItem(value: ThemeMode.light, child: Text(localizations.themeLight)), + DropdownMenuItem(value: ThemeMode.dark, child: Text(localizations.themeDark)), + ]), + ]), + Tooltip( + message: "Material 3是谷歌开源设计系统的最新版本", + child: SwitchListTile( + contentPadding: const EdgeInsets.only(left: 0, right: 5), + value: appConfiguration.useMaterial3, + onChanged: (bool value) => appConfiguration.useMaterial3 = value, + title: const Text("Material3: "), + )), + const Divider(), + ListTile( + contentPadding: const EdgeInsets.only(), + title: Text(localizations.headerExpanded), + subtitle: Text(localizations.headerExpandedSubtitle, style: const TextStyle(fontSize: 14)), + trailing: SwitchWidget( + value: appConfiguration.headerExpanded, + onChanged: (value) { + appConfiguration.headerExpanded = value; + appConfiguration.flushConfig(); + })) + ]))); + } +} diff --git a/lib/ui/desktop/left/list.dart b/lib/ui/desktop/left/list.dart index df6c8f7..bf4da2c 100644 --- a/lib/ui/desktop/left/list.dart +++ b/lib/ui/desktop/left/list.dart @@ -314,7 +314,7 @@ class _DomainRequestsState extends State { items: [ CustomPopupMenuItem( height: 35, - child: const Text("添加黑名单", style: TextStyle(fontSize: 13)), + child: Text(localizations.domainBlacklist, style: const TextStyle(fontSize: 13)), onTap: () { HostFilter.blacklist.add(Uri.parse(widget.domain).host); configuration.flushConfig(); @@ -322,7 +322,7 @@ class _DomainRequestsState extends State { }), CustomPopupMenuItem( height: 35, - child: const Text("添加白名单", style: TextStyle(fontSize: 13)), + child: Text(localizations.domainWhitelist, style: const TextStyle(fontSize: 13)), onTap: () { HostFilter.whitelist.add(Uri.parse(widget.domain).host); configuration.flushConfig(); @@ -330,7 +330,7 @@ class _DomainRequestsState extends State { }), CustomPopupMenuItem( height: 35, - child: const Text("删除白名单", style: TextStyle(fontSize: 13)), + child: Text(localizations.deleteWhitelist, style: const TextStyle(fontSize: 13)), onTap: () { HostFilter.whitelist.remove(Uri.parse(widget.domain).host); configuration.flushConfig(); @@ -338,7 +338,9 @@ class _DomainRequestsState extends State { }), const PopupMenuDivider(height: 0.3), CustomPopupMenuItem( - height: 35, child: Text(localizations.delete, style: const TextStyle(fontSize: 13)), onTap: () => _delete()), + height: 35, + child: Text(localizations.delete, style: const TextStyle(fontSize: 13)), + onTap: () => _delete()), ], ); } diff --git a/lib/ui/desktop/left/search.dart b/lib/ui/desktop/left/search.dart index 01b7502..4dcec3b 100644 --- a/lib/ui/desktop/left/search.dart +++ b/lib/ui/desktop/left/search.dart @@ -107,26 +107,26 @@ class ContentTypeSelect extends StatefulWidget { class ContentTypeState extends State { AppLocalizations get localizations => AppLocalizations.of(context)!; - late String value; - late List types; + String? value; + List? types; @override void initState() { super.initState(); - value = localizations.all; - types = ["JSON", "HTML", "JS", "CSS", "TEXT", "IMAGE", localizations.all]; } @override Widget build(BuildContext context) { - ContentType.json; + value ??= localizations.all; + types ??= ["JSON", "HTML", "JS", "CSS", "TEXT", "IMAGE", localizations.all]; + return PopupMenuButton( initialValue: value, - offset: Offset(-10, (types.length - types.indexOf(value)) * -30.0 - 10), + offset: Offset(-10, (types!.length - types!.indexOf(value!)) * -30.0 - 10), tooltip: localizations.responseType, constraints: const BoxConstraints(maxWidth: 75), child: Wrap(runAlignment: WrapAlignment.center, children: [ - Text(value, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)), + Text(value!, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)), const Icon(Icons.arrow_drop_up, size: 20) ]), onSelected: (String value) { @@ -139,7 +139,7 @@ class ContentTypeState extends State { widget.onSelected(value == localizations.all ? null : ContentType.valueOf(value)); }, itemBuilder: (BuildContext context) { - return types.map(item).toList(); + return types!.map(item).toList(); }, ); } diff --git a/lib/ui/desktop/toolbar/setting/external_proxy.dart b/lib/ui/desktop/toolbar/setting/external_proxy.dart index 944a43d..3eb5e19 100644 --- a/lib/ui/desktop/toolbar/setting/external_proxy.dart +++ b/lib/ui/desktop/toolbar/setting/external_proxy.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:network_proxy/network/bin/configuration.dart'; import 'package:network_proxy/network/host_port.dart'; @@ -20,6 +21,8 @@ class _ExternalProxyDialogState extends State { final formKey = GlobalKey(); late ProxyInfo externalProxy; + AppLocalizations get localizations => AppLocalizations.of(context)!; + @override void initState() { super.initState(); @@ -34,13 +37,13 @@ class _ExternalProxyDialogState extends State { return AlertDialog( scrollable: true, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)), - title: const Text("外部代理设置", style: TextStyle(fontSize: 15)), + title: Text(localizations.externalProxy, style: const TextStyle(fontSize: 15)), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, - child: const Text("取消")), + child: Text(localizations.cancel)), TextButton( onPressed: () async { if (!formKey.currentState!.validate()) { @@ -48,12 +51,11 @@ class _ExternalProxyDialogState extends State { } submit(); }, - child: const Text("确定")) + child: Text(localizations.confirm)) ], content: Form( key: formKey, child: Column(mainAxisSize: MainAxisSize.min, children: [ - const Text("如发现访问失败的外网请将加入域名过滤黑名单。", style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)), const SizedBox(height: 10), Row(children: [ const Text("是否启用:"), @@ -75,7 +77,7 @@ class _ExternalProxyDialogState extends State { )) ]), Row(children: [ - const Text("端口:"), + Text(localizations.port), Expanded( child: TextFormField( initialValue: externalProxy.port?.toString() ?? '', @@ -84,7 +86,7 @@ class _ExternalProxyDialogState extends State { FilteringTextInputFormatter.allow(RegExp("[0-9]")) ], onChanged: (val) => externalProxy.port = int.parse(val), - validator: (val) => val == null || val.isEmpty ? "端口不能为空" : null, + validator: (val) => val == null || val.isEmpty ? "${localizations.port}不能为空" : null, decoration: const InputDecoration(), )) ]), @@ -110,13 +112,13 @@ class _ExternalProxyDialogState extends State { onPressed: () { Navigator.of(context).pop(); }, - child: const Text("取消")), + child: Text(localizations.cancel)), TextButton( onPressed: () { setting = true; Navigator.of(context).pop(); }, - child: const Text("确定")) + child: Text(localizations.confirm)) ], )); } diff --git a/lib/ui/desktop/toolbar/setting/setting.dart b/lib/ui/desktop/toolbar/setting/setting.dart index 8a74c80..cf92b0b 100644 --- a/lib/ui/desktop/toolbar/setting/setting.dart +++ b/lib/ui/desktop/toolbar/setting/setting.dart @@ -64,11 +64,11 @@ class _SettingState extends State { }, menuChildren: [ _ProxyMenu(proxyServer: widget.proxyServer), - futureWidget(UIConfiguration.instance, (uiConfiguration) => ThemeSetting(uiConfiguration: uiConfiguration)), - item("域名过滤", onPressed: hostFilter), - item("请求重写", onPressed: requestRewrite), - item("脚本", onPressed: () => openScriptWindow()), - item("外部代理设置", onPressed: setExternalProxy), + futureWidget(AppConfiguration.instance, (appConfiguration) => ThemeSetting(appConfiguration: appConfiguration)), + item(localizations.domainFilter, onPressed: hostFilter), + item(localizations.requestRewrite, onPressed: requestRewrite), + item(localizations.script, onPressed: () => openScriptWindow()), + item(localizations.externalProxy, onPressed: setExternalProxy), item("Github", onPressed: () => launchUrl(Uri.parse("https://github.com/wanghongenpin/network_proxy_flutter"))), ], ); @@ -130,6 +130,8 @@ class _ProxyMenuState extends State<_ProxyMenu> { late Configuration configuration; bool changed = false; + AppLocalizations get localizations => AppLocalizations.of(context)!; + @override void initState() { configuration = widget.proxyServer.configuration; @@ -203,7 +205,9 @@ class _ProxyMenuState extends State<_ProxyMenu> { minLines: 1)), const SizedBox(height: 10), ], - child: const Padding(padding: EdgeInsets.only(left: 10), child: Text("代理", style: TextStyle(fontSize: 14))), + child: Padding( + padding: const EdgeInsets.only(left: 10), + child: Text(localizations.proxy, style: const TextStyle(fontSize: 14))), ); } @@ -211,7 +215,7 @@ class _ProxyMenuState extends State<_ProxyMenu> { Widget setSystemProxy() { return SwitchListTile( hoverColor: Colors.transparent, - title: const Text("设置为系统代理", maxLines: 1), + title: Text(localizations.systemProxy, maxLines: 1), dense: true, value: configuration.enableSystemProxy, onChanged: (val) { @@ -240,6 +244,8 @@ class _PortState extends State { final textController = TextEditingController(); final FocusNode portFocus = FocusNode(); + AppLocalizations get localizations => AppLocalizations.of(context)!; + @override void initState() { super.initState(); @@ -269,7 +275,7 @@ class _PortState extends State { Widget build(BuildContext context) { return Row(children: [ const Padding(padding: EdgeInsets.only(left: 15)), - Text("端口号:", style: widget.textStyle), + Text(localizations.port, style: widget.textStyle), SizedBox( width: 80, child: TextFormField( diff --git a/lib/ui/desktop/toolbar/setting/theme.dart b/lib/ui/desktop/toolbar/setting/theme.dart index 1686229..6a4f4b9 100644 --- a/lib/ui/desktop/toolbar/setting/theme.dart +++ b/lib/ui/desktop/toolbar/setting/theme.dart @@ -3,9 +3,9 @@ import 'package:network_proxy/ui/configuration.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class ThemeSetting extends StatelessWidget { - final UIConfiguration uiConfiguration; + final AppConfiguration appConfiguration; - const ThemeSetting({super.key, required this.uiConfiguration}); + const ThemeSetting({super.key, required this.appConfiguration}); @override Widget build(BuildContext context) { @@ -28,32 +28,32 @@ class ThemeSetting extends StatelessWidget { message: "Material 3是谷歌开源设计系统的最新版本", child: SwitchListTile( contentPadding: const EdgeInsets.only(left: 32, right: 5), - value: uiConfiguration.useMaterial3, - onChanged: (bool value) => uiConfiguration.useMaterial3 = value, + value: appConfiguration.useMaterial3, + onChanged: (bool value) => appConfiguration.useMaterial3 = value, dense: true, title: const Text("Material3"), ))), MenuItemButton( - leadingIcon: uiConfiguration.themeMode == ThemeMode.system + leadingIcon: appConfiguration.themeMode == ThemeMode.system ? const Icon(Icons.check, size: 15) : const SizedBox(width: 18), trailingIcon: const Icon(Icons.cached), child: Text(localizations.followSystem), - onPressed: () => uiConfiguration.themeMode = ThemeMode.system), + onPressed: () => appConfiguration.themeMode = ThemeMode.system), MenuItemButton( - leadingIcon: uiConfiguration.themeMode == ThemeMode.dark + 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: () => uiConfiguration.themeMode = ThemeMode.dark), + onPressed: () => appConfiguration.themeMode = ThemeMode.dark), MenuItemButton( - leadingIcon: uiConfiguration.themeMode == ThemeMode.light + 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: () => uiConfiguration.themeMode = ThemeMode.light), + onPressed: () => appConfiguration.themeMode = ThemeMode.light), ], child: Padding( padding: const EdgeInsets.only(left: 10), diff --git a/lib/ui/desktop/toolbar/ssl/ssl.dart b/lib/ui/desktop/toolbar/ssl/ssl.dart index 6072da4..7329fbd 100644 --- a/lib/ui/desktop/toolbar/ssl/ssl.dart +++ b/lib/ui/desktop/toolbar/ssl/ssl.dart @@ -37,18 +37,15 @@ class _SslState extends State { PopupMenuItem(child: _Switch(proxyServer: widget.proxyServer, onEnableChange: (val) => setState(() {}))), PopupMenuItem( child: ListTile( - dense: true, - hoverColor: Colors.transparent, - focusColor: Colors.transparent, - title: const Text("安装根证书到本机"), - trailing: const Icon(Icons.arrow_right), - onTap: () { - pcCer(); - }, - )), + dense: true, + hoverColor: Colors.transparent, + focusColor: Colors.transparent, + title: Text(localizations.installCaLocal), + trailing: const Icon(Icons.arrow_right), + onTap: pcCer)), PopupMenuItem( child: ListTile( - title: const Text("安装根证书到 iOS"), + title: Text("${localizations.installRootCa} iOS"), dense: true, hoverColor: Colors.transparent, focusColor: Colors.transparent, @@ -59,7 +56,7 @@ class _SslState extends State { ), PopupMenuItem( child: ListTile( - title: const Text("安装根证书到 Android"), + title: Text("${localizations.installRootCa} Android"), dense: true, hoverColor: Colors.transparent, focusColor: Colors.transparent, @@ -70,7 +67,7 @@ class _SslState extends State { ), PopupMenuItem( child: ListTile( - title: const Text("下载根证书"), + title: Text(localizations.downloadRootCa), dense: true, hoverColor: Colors.transparent, focusColor: Colors.transparent, @@ -89,12 +86,17 @@ class _SslState extends State { } void pcCer() async { + bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh'); + List list = []; if (Platform.isMacOS || Platform.isWindows) { list = [ - Text(" 安装证书到本系统,${Platform.isMacOS ? "“安装完双击选择“始终信任此证书”。 如安装打开失败,请下载证书拖拽到系统证书里" : "选择“受信任的根证书颁发机构”"}"), + isCN + ? Text(" 安装证书到本系统,${Platform.isMacOS ? "安装完双击选择“始终信任此证书”。 如安装打开失败,请下载证书拖拽到系统证书里" : "选择“受信任的根证书颁发机构”"}") + : Text(" Install certificate to this system,${Platform.isMacOS ? "After installation, double-click to select “Always Trust”。\n" + " If installation and opening fail,Please download the certificate and drag it to the system certificate" : "choice“Trusted Root Certificate Authority”"}"), const SizedBox(height: 10), - FilledButton(onPressed: _installCert, child: const Text("安装证书")), + FilledButton(onPressed: _installCert, child: Text(localizations.installRootCa)), const SizedBox(height: 10), Platform.isMacOS ? Image.network("https://foruda.gitee.com/images/1689323260158189316/c2d881a4_1073801.png", @@ -125,12 +127,14 @@ class _SslState extends State { showDialog( context: context, + barrierDismissible: false, builder: (BuildContext context) { return SimpleDialog( contentPadding: const EdgeInsets.all(16), - title: const Row(children: [ - Text("电脑HTTPS抓包配置", style: TextStyle(fontSize: 16)), - Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton())) + title: Row(children: [ + Text(isCN ? "电脑HTTPS抓包配置" : "Computer HTTPS Packet Capture Configuration", + style: const TextStyle(fontSize: 16)), + const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton())) ]), alignment: Alignment.center, children: list); @@ -140,6 +144,7 @@ class _SslState extends State { void iosCer(String host) { showDialog( context: context, + barrierDismissible: false, builder: (BuildContext context) { return SimpleDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)), @@ -160,18 +165,18 @@ class _SslState extends State { ], ), const SizedBox(height: 10), - const Text("3. 安装根证书并信任证书"), + Text("3. ${localizations.installRootCa} -> ${localizations.trustCa}"), const SizedBox(height: 10), Row(children: [ Column(children: [ - const Text("3.1 安装证书 设置 > 已下载描述文件 > 安装", style: TextStyle(fontSize: 12)), + Text("3.1 ${localizations.installCaDescribe}", style: const TextStyle(fontSize: 12)), const SizedBox(height: 10), Image.network("https://foruda.gitee.com/images/1689346516243774963/c56bc546_1073801.png", height: 270, width: 300) ]), const SizedBox(width: 10), Column(children: [ - const Text("3.2 信任证书 设置 > 通用 > 关于本机 > 证书信任设置", style: TextStyle(fontSize: 12)), + Text("3.2 ${localizations.trustCaDescribe}", style: const TextStyle(fontSize: 12)), const SizedBox(height: 10), Image.network("https://foruda.gitee.com/images/1689346614916658100/fd9b9e41_1073801.png", height: 270, width: 300) @@ -184,6 +189,7 @@ class _SslState extends State { void androidCer(String host) { showDialog( context: context, + barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)), @@ -197,8 +203,8 @@ class _SslState extends State { width: 600, child: ListView(children: [ ExpansionTile( - title: - const Text("ROOT用户: 安装到系统证书", style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14)), + title: Text(localizations.androidRoot, + style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14)), tilePadding: const EdgeInsets.only(left: 0), expandedCrossAxisAlignment: CrossAxisAlignment.start, expandedAlignment: Alignment.topLeft, @@ -206,18 +212,18 @@ class _SslState extends State { initiallyExpanded: true, shape: const Border(), children: [ - const Text("针对安卓ROOT用户做了个Magisk模块ProxyPinCA系统证书,安装完重启手机即可。"), + Text(localizations.androidRootMagisk), TextButton( child: const Text("https://gitee.com/wanghongenpin/Magisk-ProxyPinCA/releases"), onPressed: () { launchUrl(Uri.parse("https://gitee.com/wanghongenpin/Magisk-ProxyPinCA/releases")); }), - const SelectableText("模块不生效可以根据网上教程安装系统根证书, 根证书命名成 243f0bfb.0"), + SelectableText(localizations.androidRootRename), ]), const SizedBox(height: 10), ExpansionTile( - title: const Text("非ROOT用户: 安装到用户证书(很多软件不会信任用户证书)", - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14)), + title: Text(localizations.androidUserCA, + style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14)), tilePadding: const EdgeInsets.only(left: 0), expandedCrossAxisAlignment: CrossAxisAlignment.start, childrenPadding: const EdgeInsets.only(left: 20), @@ -235,14 +241,14 @@ class _SslState extends State { ], ), const SizedBox(height: 10), - const Text("3. 打开设置 -> 安全 -> 加密和凭据 -> 安装证书 -> CA 证书"), + Text("3. ${localizations.androidUserCAInstall}"), const SizedBox(height: 10), TextButton( onPressed: () { launchUrl(Uri.parse( "https://gitee.com/wanghongenpin/network-proxy-flutter/wikis/%E5%AE%89%E5%8D%93%E6%97%A0ROOT%E4%BD%BF%E7%94%A8Xposed%E6%A8%A1%E5%9D%97%E6%8A%93%E5%8C%85")); }, - child: const Text(" 推荐使用Xposed模块抓包(无需ROOT), 点击查看wiki")), + child: Text(" ${localizations.androidUserXposed}")), ClipRRect( child: Align( alignment: Alignment.topCenter, diff --git a/lib/ui/mobile/menu.dart b/lib/ui/mobile/menu.dart index 0aacaab..66575b0 100644 --- a/lib/ui/mobile/menu.dart +++ b/lib/ui/mobile/menu.dart @@ -80,8 +80,8 @@ class DrawerWidget extends StatelessWidget { leading: const Icon(Icons.settings), onTap: () => navigator( context, - futureWidget(UIConfiguration.instance, - (uiConfiguration) => SettingMenu(proxyServer: proxyServer, uiConfiguration: uiConfiguration)))), + futureWidget(AppConfiguration.instance, + (appConfiguration) => SettingMenu(proxyServer: proxyServer, appConfiguration: appConfiguration)))), ListTile( title: Text(localizations.about), leading: const Icon(Icons.info_outline), @@ -103,9 +103,9 @@ navigator(BuildContext context, Widget widget) { ///设置 class SettingMenu extends StatelessWidget { final ProxyServer proxyServer; - final UIConfiguration uiConfiguration; + final AppConfiguration appConfiguration; - const SettingMenu({super.key, required this.proxyServer, required this.uiConfiguration}); + const SettingMenu({super.key, required this.proxyServer, required this.appConfiguration}); @override Widget build(BuildContext context) { @@ -125,18 +125,27 @@ class SettingMenu extends StatelessWidget { trailing: const Icon(Icons.arrow_right), onTap: () => _language(context), ), - MobileThemeSetting(uiConfiguration: uiConfiguration), + MobileThemeSetting(appConfiguration: appConfiguration), Platform.isIOS ? const SizedBox() : ListTile( title: Text(localizations.windowMode), subtitle: Text(localizations.windowModeSubTitle, style: const TextStyle(fontSize: 12)), trailing: SwitchWidget( - value: proxyServer.configuration.smallWindow, + value: appConfiguration.smallWindow, onChanged: (value) { - proxyServer.configuration.smallWindow = value; - proxyServer.configuration.flushConfig(); + appConfiguration.smallWindow = value; + appConfiguration.flushConfig(); })), + ListTile( + title: Text(localizations.headerExpanded), + subtitle: Text(localizations.headerExpandedSubtitle, style: const TextStyle(fontSize: 12)), + trailing: SwitchWidget( + value: appConfiguration.headerExpanded, + onChanged: (value) { + appConfiguration.headerExpanded = value; + appConfiguration.flushConfig(); + })) ]))); } @@ -154,14 +163,14 @@ class SettingMenu extends StatelessWidget { children: [ TextButton( onPressed: () { - uiConfiguration.language = null; + appConfiguration.language = null; Navigator.of(context).pop(); }, child: Text(localizations.followSystem)), const Divider(thickness: 0.5, height: 0), TextButton( onPressed: () { - uiConfiguration.language = const Locale.fromSubtags(languageCode: 'zh'); + appConfiguration.language = const Locale.fromSubtags(languageCode: 'zh'); Navigator.of(context).pop(); }, child: const Text("简体中文")), @@ -169,7 +178,7 @@ class SettingMenu extends StatelessWidget { TextButton( child: const Text("English"), onPressed: () { - uiConfiguration.language = const Locale.fromSubtags(languageCode: 'en'); + appConfiguration.language = const Locale.fromSubtags(languageCode: 'en'); Navigator.of(context).pop(); }), const Divider(thickness: 0.5), diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index 8ce74bc..04d6ed0 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -16,19 +16,20 @@ import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/http/websocket.dart'; import 'package:network_proxy/network/http_client.dart'; import 'package:network_proxy/ui/component/utils.dart'; +import 'package:network_proxy/ui/configuration.dart'; import 'package:network_proxy/ui/content/panel.dart'; import 'package:network_proxy/ui/launch/launch.dart'; import 'package:network_proxy/ui/mobile/connect_remote.dart'; import 'package:network_proxy/ui/mobile/menu.dart'; import 'package:network_proxy/ui/mobile/request/list.dart'; import 'package:network_proxy/ui/mobile/request/search.dart'; -import 'package:network_proxy/ui/configuration.dart'; import 'package:network_proxy/utils/ip.dart'; class MobileHomePage extends StatefulWidget { final Configuration configuration; + final AppConfiguration appConfiguration; - const MobileHomePage({super.key, required this.configuration}); + const MobileHomePage(this.configuration, this.appConfiguration, {super.key}); @override State createState() { @@ -36,17 +37,23 @@ class MobileHomePage extends StatefulWidget { } } +///画中画 +final ValueNotifier pictureInPictureNotifier = ValueNotifier(false); + class MobileHomeState extends State implements EventListener, LifecycleListener { static GlobalKey requestStateKey = GlobalKey(); + /// 远程连接 + final ValueNotifier desktop = ValueNotifier(RemoteModel(connect: false)); + late ProxyServer proxyServer; AppLocalizations get localizations => AppLocalizations.of(context)!; @override - void onUserLeaveHint() { - if (Vpn.isVpnStarted && !AppConfiguration.pictureInPictureNotifier.value) { - if (AppConfiguration.desktop.value.connect || !Platform.isAndroid || !widget.configuration.smallWindow) { + void onUserLeaveHint() async { + if (Vpn.isVpnStarted && !pictureInPictureNotifier.value) { + if (desktop.value.connect || !Platform.isAndroid || !(await (AppConfiguration.instance)).smallWindow) { return; } @@ -56,18 +63,18 @@ class MobileHomeState extends State implements EventListener, Li @override onPictureInPictureModeChanged(bool isInPictureInPictureMode) { - if (isInPictureInPictureMode && !AppConfiguration.pictureInPictureNotifier.value) { + if (isInPictureInPictureMode && !pictureInPictureNotifier.value) { while (Navigator.canPop(context)) { Navigator.pop(context); } - AppConfiguration.pictureInPictureNotifier.value = true; + pictureInPictureNotifier.value = true; return; } - if (!isInPictureInPictureMode && AppConfiguration.pictureInPictureNotifier.value) { + if (!isInPictureInPictureMode && pictureInPictureNotifier.value) { Vpn.isRunning().then((value) { Vpn.isVpnStarted = value; - AppConfiguration.pictureInPictureNotifier.value = false; + pictureInPictureNotifier.value = false; }); } } @@ -99,7 +106,6 @@ class MobileHomeState extends State implements EventListener, Li proxyServer.start(); //远程连接 - final desktop = AppConfiguration.desktop; desktop.addListener(() { if (desktop.value.connect) { proxyServer.configuration.remoteHost = "http://${desktop.value.host}:${desktop.value.port}"; @@ -109,7 +115,7 @@ class MobileHomeState extends State implements EventListener, Li } }); - if (widget.configuration.upgradeNoticeV6) { + if (widget.appConfiguration.upgradeNoticeV7) { WidgetsBinding.instance.addPostFrameCallback((_) { showUpgradeNotice(); }); @@ -118,7 +124,7 @@ class MobileHomeState extends State implements EventListener, Li @override void dispose() { - AppConfiguration.desktop.dispose(); + desktop.dispose(); AppLifecycleBinding.instance.removeListener(this); super.dispose(); } @@ -142,7 +148,7 @@ class MobileHomeState extends State implements EventListener, Li SystemNavigator.pop(); }, child: ValueListenableBuilder( - valueListenable: AppConfiguration.pictureInPictureNotifier, + valueListenable: pictureInPictureNotifier, builder: (context, pip, _) { if (pip) { return Scaffold(body: RequestListWidget(key: requestStateKey, proxyServer: proxyServer)); @@ -153,7 +159,7 @@ class MobileHomeState extends State implements EventListener, Li drawer: DrawerWidget(proxyServer: proxyServer), floatingActionButton: _floatingActionButton(), body: ValueListenableBuilder( - valueListenable: AppConfiguration.desktop, + valueListenable: desktop, builder: (context, value, _) { return Column(children: [ value.connect ? remoteConnect(value) : const SizedBox(), @@ -171,7 +177,7 @@ class MobileHomeState extends State implements EventListener, Li icon: const Icon(Icons.cleaning_services_outlined), onPressed: () => requestStateKey.currentState?.clean()), const SizedBox(width: 2), - MoreMenu(proxyServer: proxyServer, desktop: AppConfiguration.desktop), + MoreMenu(proxyServer: proxyServer, desktop: desktop), const SizedBox(width: 10) ]); } @@ -195,17 +201,23 @@ class MobileHomeState extends State implements EventListener, Li } showUpgradeNotice() { - String content = '提示:默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n\n' - '1. 请求重写增加 修改请求,可根据增则替换;\n' - '2. 请求重写批量导入、导出;\n' - '3. 支持WebSocket抓包;\n' - '4. 安卓支持小窗口模式;\n' - '5. 优化curl导入;\n' - '6. 支持head请求,修复手机端请求重写切换应用恢复原始的请求问题;\n' - ''; - showAlertDialog('更新内容V1.0.6', content, () { - widget.configuration.upgradeNoticeV6 = false; - widget.configuration.flushConfig(); + bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh'); + + String content = isCN + ? '提示:默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n\n' + '1. 请求重写增加 修改请求,可根据正则替换;\n' + '2. 请求重写批量导入、导出;\n' + '3. 支持WebSocket抓包;\n' + '4. 安卓支持小窗口模式;\n' + '5. 优化curl导入;\n' + '6. 支持head请求,修复手机端请求重写切换应用恢复原始的请求问题;' + : 'Tips:By default, HTTPS packet capture will not be enabled. Please install the certificate before enabling HTTPS packet capture。\n\n' + '1. Increase multilingual support;\n' + '2. Details page Headers Expanded Config;\n' + ; + showAlertDialog(isCN ? '更新内容V1.0.6' : "Update content V1.0.6", content, () { + widget.appConfiguration.upgradeNoticeV7 = false; + widget.appConfiguration.flushConfig(); }); } @@ -217,7 +229,7 @@ class MobileHomeState extends State implements EventListener, Li width: double.infinity, child: ElevatedButton( onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) { - return ConnectRemote(desktop: AppConfiguration.desktop, proxyServer: proxyServer); + return ConnectRemote(desktop: desktop, proxyServer: proxyServer); })), child: Text("已连接${value.os?.toUpperCase()},手机抓包已关闭", style: Theme.of(context).textTheme.titleMedium), )); @@ -247,7 +259,6 @@ class MobileHomeState extends State implements EventListener, Li checkConnectTask(BuildContext context) async { int retry = 0; Timer.periodic(const Duration(milliseconds: 3000), (timer) async { - var desktop = AppConfiguration.desktop; if (desktop.value.connect == false) { timer.cancel(); return; diff --git a/lib/ui/mobile/request/favorite.dart b/lib/ui/mobile/request/favorite.dart index ab93582..7fdd050 100644 --- a/lib/ui/mobile/request/favorite.dart +++ b/lib/ui/mobile/request/favorite.dart @@ -221,7 +221,7 @@ class _FavoriteItemState extends State<_FavoriteItem> { proxyServer: widget.proxyServer, httpRequest: request, httpResponse: request.response, - title: Text(localizations.captureDetail, style: TextStyle(fontSize: 16))); + title: Text(localizations.captureDetail, style: const TextStyle(fontSize: 16))); })); } } diff --git a/lib/ui/mobile/request/list.dart b/lib/ui/mobile/request/list.dart index 5e3c23e..4cec07d 100644 --- a/lib/ui/mobile/request/list.dart +++ b/lib/ui/mobile/request/list.dart @@ -11,8 +11,8 @@ import 'package:network_proxy/network/components/host_filter.dart'; import 'package:network_proxy/network/host_port.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/ui/desktop/left/model/search_model.dart'; +import 'package:network_proxy/ui/mobile/mobile.dart'; import 'package:network_proxy/ui/mobile/request/request.dart'; -import 'package:network_proxy/ui/configuration.dart'; import 'package:network_proxy/utils/lang.dart'; class RequestListWidget extends StatefulWidget { @@ -46,7 +46,7 @@ class RequestListState extends State { @override Widget build(BuildContext context) { - if (AppConfiguration.pictureInPictureNotifier.value) { + if (pictureInPictureNotifier.value) { if (container.isEmpty) { return Center(child: Text(localizations.emptyData, style: const TextStyle(color: Colors.grey))); } @@ -85,7 +85,7 @@ class RequestListState extends State { ///添加请求 add(Channel channel, HttpRequest request) { - if (AppConfiguration.pictureInPictureNotifier.value) { + if (pictureInPictureNotifier.value) { setState(() { container.add(request); }); diff --git a/lib/ui/mobile/setting/proxy.dart b/lib/ui/mobile/setting/proxy.dart index 668c4ce..08a316a 100644 --- a/lib/ui/mobile/setting/proxy.dart +++ b/lib/ui/mobile/setting/proxy.dart @@ -6,6 +6,7 @@ import 'package:network_proxy/network/bin/configuration.dart'; import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/network/host_port.dart'; import 'package:network_proxy/ui/desktop/toolbar/setting/setting.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class ProxySetting extends StatefulWidget { final ProxyServer proxyServer; @@ -19,15 +20,17 @@ class ProxySetting extends StatefulWidget { } class _ProxySettingState extends State { + AppLocalizations get localizations => AppLocalizations.of(context)!; + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('代理设置', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500))), + appBar: AppBar(title: Text(localizations.proxySetting, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500))), body: ListView(children: [ PortWidget(proxyServer: widget.proxyServer), const Divider(height: 20, thickness: 0.3), ListTile( - title: const Text('外部代理'), + title: Text(localizations.externalProxy), trailing: const Icon(Icons.keyboard_arrow_right), onTap: () { showDialog( @@ -54,6 +57,8 @@ class _ExternalProxyDialogState extends State { final formKey = GlobalKey(); late ProxyInfo externalProxy; + AppLocalizations get localizations => AppLocalizations.of(context)!; + @override void initState() { super.initState(); @@ -67,13 +72,9 @@ class _ExternalProxyDialogState extends State { Widget build(BuildContext context) { return AlertDialog( scrollable: true, - title: const Text("外部代理设置", style: TextStyle(fontSize: 15)), + title: Text(localizations.externalProxy, style: const TextStyle(fontSize: 15)), actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text("取消")), + TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(localizations.cancel)), TextButton( onPressed: () async { if (!formKey.currentState!.validate()) { @@ -81,7 +82,7 @@ class _ExternalProxyDialogState extends State { } submit(); }, - child: const Text("确定")) + child: Text(localizations.confirm)) ], content: Form( key: formKey, diff --git a/lib/ui/mobile/setting/ssl.dart b/lib/ui/mobile/setting/ssl.dart index 8098eb7..b62492b 100644 --- a/lib/ui/mobile/setting/ssl.dart +++ b/lib/ui/mobile/setting/ssl.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/network/util/crts.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -18,6 +19,8 @@ class MobileSslWidget extends StatefulWidget { class _MobileSslState extends State { bool changed = false; + AppLocalizations get localizations => AppLocalizations.of(context)!; + @override void dispose() { if (changed) { @@ -30,13 +33,13 @@ class _MobileSslState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("HTTPS代理", style: TextStyle(fontSize: 16)), + title: Text(localizations.httpsProxy, style: const TextStyle(fontSize: 16)), centerTitle: true, ), body: ListView(children: [ SwitchListTile( hoverColor: Colors.transparent, - title: const Text("启用HTTPS代理"), + title: Text(localizations.enabledHttps), value: widget.proxyServer.enableSsl, onChanged: (val) { widget.proxyServer.enableSsl = val; @@ -52,21 +55,21 @@ class _MobileSslState extends State { Widget ios() { return ExpansionTile( - title: const Text("安装根证书"), + title: Text(localizations.profileDownload), initiallyExpanded: true, childrenPadding: const EdgeInsets.only(left: 20), expandedAlignment: Alignment.topLeft, expandedCrossAxisAlignment: CrossAxisAlignment.start, shape: const Border(), children: [ - TextButton(onPressed: () => _downloadCert(), child: const Text("1. 点击下载根证书")), - TextButton(onPressed: () {}, child: const Text("2. 安装根证书 -> 信任证书")), - TextButton(onPressed: () {}, child: const Text("2.1 安装根证书 设置 > 已下载描述文件 > 安装")), + TextButton(onPressed: () => _downloadCert(), child: Text("1. ${localizations.downloadRootCa}")), + TextButton(onPressed: () {}, child: Text("2. ${localizations.installRootCa} -> ${localizations.trustCa}")), + TextButton(onPressed: () {}, child: Text("2.1 ${localizations.installCaDescribe}")), Padding( padding: const EdgeInsets.only(left: 15), child: Image.network("https://foruda.gitee.com/images/1689346516243774963/c56bc546_1073801.png", height: 400)), - TextButton(onPressed: () {}, child: const Text("2.2 信任根证书 设置 > 通用 > 关于本机 -> 证书信任设置")), + TextButton(onPressed: () {}, child: Text("2.2 ${localizations.trustCaDescribe}")), Padding( padding: const EdgeInsets.only(left: 15), child: Image.network("https://foruda.gitee.com/images/1689346614916658100/fd9b9e41_1073801.png", @@ -76,26 +79,25 @@ class _MobileSslState extends State { Widget android() { return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text("安装根证书"), + Text(localizations.installRootCa), ExpansionTile( - title: const Text("ROOT用户: 安装到系统证书", style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14)), + title: Text(localizations.androidRoot, style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14)), tilePadding: const EdgeInsets.only(left: 0), expandedAlignment: Alignment.topLeft, initiallyExpanded: true, shape: const Border(), children: [ - const Text("针对安卓Root用户做了个Magisk模块ProxyPinCA系统证书,安装完重启手机即可。"), + Text(localizations.androidRootMagisk), TextButton( child: const Text("https://gitee.com/wanghongenpin/Magisk-ProxyPinCA/releases"), onPressed: () { launchUrl(Uri.parse("https://gitee.com/wanghongenpin/Magisk-ProxyPinCA/releases")); }), - const SelectableText("模块不生效可以根据网上教程安装系统根证书, 根证书命名成 243f0bfb.0"), + SelectableText(localizations.androidRootRename), ]), const SizedBox(height: 20), ExpansionTile( - title: - const Text("非ROOT用户: 安装到用户证书(很多软件不会信任用户证书)", style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14)), + title: Text(localizations.androidUserCA, style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14)), tilePadding: const EdgeInsets.only(left: 0), expandedAlignment: Alignment.topLeft, expandedCrossAxisAlignment: CrossAxisAlignment.start, @@ -105,16 +107,16 @@ class _MobileSslState extends State { TextButton( onPressed: () => _downloadCert(), child: Text.rich(TextSpan(children: [ - const TextSpan(text: "1. 点击下载根证书 "), + TextSpan(text: "1. ${localizations.downloadRootCa} "), WidgetSpan(child: SelectableText("http://127.0.0.1:${widget.proxyServer.port}/ssl")) ]))), - TextButton(onPressed: () {}, child: const Text("2. 打开设置 -> 安全 -> 加密和凭据 -> 安装证书 -> CA 证书")), + TextButton(onPressed: () {}, child: Text("2. ${localizations.androidUserCAInstall}")), TextButton( onPressed: () { launchUrl(Uri.parse( "https://gitee.com/wanghongenpin/network-proxy-flutter/wikis/%E5%AE%89%E5%8D%93%E6%97%A0ROOT%E4%BD%BF%E7%94%A8Xposed%E6%A8%A1%E5%9D%97%E6%8A%93%E5%8C%85")); }, - child: const Text("推荐使用Xposed模块抓包(无需ROOT), 点击查看wiki")), + child: Text(localizations.androidUserXposed)), ClipRRect( child: Align( alignment: Alignment.topCenter, diff --git a/lib/ui/mobile/setting/theme.dart b/lib/ui/mobile/setting/theme.dart index 0bcf7fa..3468758 100644 --- a/lib/ui/mobile/setting/theme.dart +++ b/lib/ui/mobile/setting/theme.dart @@ -3,16 +3,16 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:network_proxy/ui/configuration.dart'; class MobileThemeSetting extends StatelessWidget { - final UIConfiguration uiConfiguration; + final AppConfiguration appConfiguration; - const MobileThemeSetting({super.key, required this.uiConfiguration}); + const MobileThemeSetting({super.key, required this.appConfiguration}); @override Widget build(BuildContext context) { AppLocalizations localizations = AppLocalizations.of(context)!; return PopupMenuButton( - tooltip: uiConfiguration.themeMode.name, + tooltip: appConfiguration.themeMode.name, surfaceTintColor: Theme.of(context).colorScheme.onPrimary, offset: const Offset(150, 0), itemBuilder: (BuildContext context) { @@ -22,9 +22,9 @@ class MobileThemeSetting extends StatelessWidget { preferBelow: false, message: "Material 3是谷歌开源设计系统的最新版本", child: SwitchListTile( - value: uiConfiguration.useMaterial3, + value: appConfiguration.useMaterial3, onChanged: (bool value) { - uiConfiguration.useMaterial3 = value; + appConfiguration.useMaterial3 = value; Navigator.of(context).pop(); }, dense: true, @@ -33,14 +33,14 @@ class MobileThemeSetting extends StatelessWidget { PopupMenuItem( child: ListTile(trailing: const Icon(Icons.cached), dense: true, title: Text(localizations.followSystem)), - onTap: () => uiConfiguration.themeMode = ThemeMode.system), + onTap: () => appConfiguration.themeMode = ThemeMode.system), PopupMenuItem( child: ListTile(trailing: const Icon(Icons.sunny), dense: true, title: Text(localizations.themeLight)), - onTap: () => uiConfiguration.themeMode = ThemeMode.light), + onTap: () => appConfiguration.themeMode = ThemeMode.light), PopupMenuItem( child: ListTile( trailing: const Icon(Icons.nightlight_outlined), dense: true, title: Text(localizations.themeDark)), - onTap: () => uiConfiguration.themeMode = ThemeMode.dark), + onTap: () => appConfiguration.themeMode = ThemeMode.dark), ]; }, child: ListTile( @@ -50,7 +50,7 @@ class MobileThemeSetting extends StatelessWidget { } Icon getIcon() { - switch (uiConfiguration.themeMode) { + switch (appConfiguration.themeMode) { case ThemeMode.system: return const Icon(Icons.cached); case ThemeMode.dark: diff --git a/test/widget_test.dart b/test/widget_test.dart index 638a7ff..b684227 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -17,8 +17,8 @@ void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. Configuration configuration = await Configuration.instance; - UIConfiguration uiConfiguration = await UIConfiguration.instance; - await tester.pumpWidget(FluentApp(DesktopHomePage(configuration: configuration), uiConfiguration: uiConfiguration)); + AppConfiguration appConfiguration = await AppConfiguration.instance; + await tester.pumpWidget(FluentApp(DesktopHomePage(configuration, appConfiguration), appConfiguration)); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget);