From d6504c6bb00a5f86c26e80777618773da61f06bb Mon Sep 17 00:00:00 2001 From: wanghongen Date: Sun, 8 Oct 2023 21:46:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A1=8C=E9=9D=A2=E7=AB=AF=E5=A2=9E=E5=8A=A0JS?= =?UTF-8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main.dart | 37 +- lib/network/channel.dart | 10 +- lib/network/handler.dart | 52 +- lib/network/http/http.dart | 7 +- lib/network/http/http_headers.dart | 15 + lib/network/http_client.dart | 6 +- lib/network/network.dart | 4 +- lib/network/util/request_rewrite.dart | 3 +- lib/network/util/script_manager.dart | 274 +++++++++++ lib/storage/favorites.dart | 1 + lib/ui/component/multi_window.dart | 86 +++- lib/ui/component/widgets.dart | 49 ++ lib/ui/desktop/left/history.dart | 11 +- lib/ui/desktop/left/request_editor.dart | 16 + lib/ui/desktop/toolbar/setting/script.dart | 463 ++++++++++++++++++ lib/ui/desktop/toolbar/setting/setting.dart | 8 +- lib/ui/desktop/toolbar/setting/theme.dart | 2 +- lib/ui/launch/launch.dart | 4 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + .../AppIcon.appiconset/Contents.json | 6 + pubspec.lock | 136 ++++- pubspec.yaml | 6 +- test/js_test.dart | 34 ++ test/tests.dart | 9 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 28 files changed, 1152 insertions(+), 98 deletions(-) create mode 100644 lib/network/util/script_manager.dart create mode 100644 lib/ui/desktop/toolbar/setting/script.dart create mode 100644 test/js_test.dart diff --git a/lib/main.dart b/lib/main.dart index 43170cb..34b0251 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,23 +1,19 @@ import 'dart:convert'; import 'dart:io'; -import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:network_proxy/network/bin/configuration.dart'; import 'package:network_proxy/ui/component/chinese_font.dart'; -import 'package:network_proxy/ui/component/encoder.dart'; -import 'package:network_proxy/ui/content/body.dart'; +import 'package:network_proxy/ui/component/multi_window.dart'; import 'package:network_proxy/ui/desktop/desktop.dart'; -import 'package:network_proxy/ui/desktop/left/request_editor.dart'; import 'package:network_proxy/ui/mobile/mobile.dart'; import 'package:network_proxy/ui/ui_configuration.dart'; import 'package:network_proxy/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; -import 'network/http/http.dart'; - void main(List args) async { WidgetsFlutterBinding.ensureInitialized(); + var instance = UIConfiguration.instance; //多窗口 if (args.firstOrNull == 'multi_window') { @@ -28,6 +24,7 @@ void main(List args) async { } var configuration = Configuration.instance; + //移动端 if (Platforms.isMobile()) { var uiConfiguration = await instance; runApp(FluentApp(MobileHomePage(configuration: (await configuration)), uiConfiguration: uiConfiguration)); @@ -51,32 +48,6 @@ void main(List args) async { runApp(FluentApp(DesktopHomePage(configuration: await configuration), uiConfiguration: uiConfiguration)); } -///多窗口 -Widget multiWindow(int windowId, Map argument) { - if (argument['name'] == 'RequestEditor') { - return RequestEditor( - windowController: WindowController.fromWindowId(windowId), - request: argument['request'] == null ? null : HttpRequest.fromJson(argument['request'])); - } - - if (argument['name'] == 'HttpBodyWidget') { - return HttpBodyWidget( - windowController: WindowController.fromWindowId(windowId), - httpMessage: HttpMessage.fromJson(argument['httpMessage']), - inNewWindow: true, - hideRequestRewrite: true); - } - - if (argument['name'] == 'EncoderWidget') { - return EncoderWidget( - type: EncoderType.nameOf(argument['type']), - text: argument['text'], - windowController: WindowController.fromWindowId(windowId)); - } - - return const SizedBox(); -} - class ThemeModel { ThemeMode mode; bool useMaterial3; @@ -89,7 +60,7 @@ class ThemeModel { ); } -/// 主题 +///主题 late ValueNotifier themeNotifier; class FluentApp extends StatelessWidget { diff --git a/lib/network/channel.dart b/lib/network/channel.dart index 2cb091c..dfd660a 100644 --- a/lib/network/channel.dart +++ b/lib/network/channel.dart @@ -85,7 +85,7 @@ class Channel { Future write(Object obj) async { if (isClosed) { - logger.w("[$id] channel is closed $obj"); + logger.w("[$id] channel is closed"); return; } @@ -98,8 +98,14 @@ class Channel { isWriting = true; try { var data = pipeline._encoder.encode(obj); - _socket.add(data); + if (!isClosed) { + _socket.add(data); + } await _socket.flush(); + } catch (e, t) { + print(getAttribute(id)._attributes); + print(e); + print(t); } finally { isWriting = false; } diff --git a/lib/network/handler.dart b/lib/network/handler.dart index b80a0fd..8362ee0 100644 --- a/lib/network/handler.dart +++ b/lib/network/handler.dart @@ -25,6 +25,7 @@ import 'package:network_proxy/network/util/attribute_keys.dart'; import 'package:network_proxy/network/util/file_read.dart'; import 'package:network_proxy/network/util/host_filter.dart'; import 'package:network_proxy/network/util/request_rewrite.dart'; +import 'package:network_proxy/network/util/script_manager.dart'; import 'package:network_proxy/utils/ip.dart'; import 'channel.dart'; @@ -126,15 +127,20 @@ class HttpChannelHandler extends ChannelHandler { //实现抓包代理转发 if (httpRequest.method != HttpMethod.connect) { - log.i("[${channel.id}] ${httpRequest.method.name} ${httpRequest.requestUrl}"); - - //替换请求体 - _rewriteBody(httpRequest); - - if (!HostFilter.filter(httpRequest.hostAndPort?.host)) { - listener?.onRequest(channel, httpRequest); + // log.i("[${channel.id}] ${httpRequest.method.name} ${httpRequest.requestUrl}"); + if (HostFilter.filter(httpRequest.hostAndPort?.host)) { + await remoteChannel.write(httpRequest); + return; } + //脚本替换 + var scriptManager = await ScriptManager.instance; + httpRequest = await scriptManager.runScript(httpRequest); + //替换请求体 + rewriteBody(httpRequest); + + listener?.onRequest(channel, httpRequest); + //重定向 var redirectRewrite = requestRewrites?.findRequestRewrite(httpRequest.hostAndPort?.host, httpRequest.path(), RuleType.redirect); @@ -152,7 +158,7 @@ class HttpChannelHandler extends ChannelHandler { } //替换请求体 - _rewriteBody(HttpRequest httpRequest) { + rewriteBody(HttpRequest httpRequest) { var rewrite = requestRewrites?.findRequestRewrite(httpRequest.hostAndPort?.host, httpRequest.path(), RuleType.body); if (rewrite?.requestBody?.isNotEmpty == true) { @@ -222,7 +228,7 @@ class HttpChannelHandler extends ChannelHandler { /// 异常处理 _exceptionHandler(Channel channel, HttpRequest? request, error) { HostAndPort? hostAndPort = channel.getAttribute(AttributeKeys.host); - hostAndPort ??= HostAndPort.host(scheme:HostAndPort.httpScheme, channel.remoteAddress.host, channel.remotePort); + hostAndPort ??= HostAndPort.host(scheme: HostAndPort.httpScheme, channel.remoteAddress.host, channel.remotePort); String message = error.toString(); HttpStatus status = HttpStatus(-1, message); if (error is HandshakeException) { @@ -231,7 +237,10 @@ class HttpChannelHandler extends ChannelHandler { status = HttpStatus(-3, error.message); } else if (error is SocketException) { status = HttpStatus(-4, error.message); + } else if (error is SignalException) { + status.reason('执行脚本异常'); } + request ??= HttpRequest(HttpMethod.connect, hostAndPort.domain) ..body = message.codeUnits ..headers.contentLength = message.codeUnits.length @@ -241,6 +250,7 @@ class HttpChannelHandler extends ChannelHandler { ..headers.contentType = 'text/plain' ..headers.contentLength = message.codeUnits.length ..body = message.codeUnits; + listener?.onRequest(channel, request); listener?.onResponse(channel, request.response!); } @@ -260,17 +270,29 @@ class HttpResponseProxyHandler extends ChannelHandler { void channelRead(Channel channel, HttpResponse msg) async { msg.request = clientChannel.getAttribute(AttributeKeys.request); msg.request?.response = msg; - // log.i("[${clientChannel.id}] Response ${msg}"); + //域名是否过滤 + if (HostFilter.filter(msg.request?.hostAndPort?.host) || msg.request?.method == HttpMethod.connect) { + await clientChannel.write(msg); + return; + } + + // log.i("[${clientChannel.id}] Response $msg"); + //脚本替换 + var scriptManager = await ScriptManager.instance; + try { + msg = await scriptManager.runResponseScript(msg); + } catch (e, t) { + msg.status = HttpStatus(-1, '执行脚本异常'); + msg.body = "$e\n${msg.bodyAsString}".codeUnits; + log.e('[${clientChannel.id}] 执行脚本异常 ', error: e, stackTrace: t); + } var replaceBody = requestRewrites?.findResponseReplaceWith(msg.request?.hostAndPort?.host, msg.request?.path()); if (replaceBody?.isNotEmpty == true) { msg.body = utf8.encode(replaceBody!); } - if (!HostFilter.filter(msg.request?.hostAndPort?.host) && msg.request?.method != HttpMethod.connect) { - listener?.onResponse(clientChannel, msg); - } - + listener?.onResponse(clientChannel, msg); //发送给客户端 await clientChannel.write(msg); } @@ -287,7 +309,7 @@ class RelayHandler extends ChannelHandler { RelayHandler(this.remoteChannel); @override - void channelRead(Channel channel, Object msg) { + void channelRead(Channel channel, Object msg) async { //发送给客户端 remoteChannel.write(msg); } diff --git a/lib/network/http/http.dart b/lib/network/http/http.dart index 752a89b..bbb26eb 100644 --- a/lib/network/http/http.dart +++ b/lib/network/http/http.dart @@ -87,6 +87,7 @@ class HttpRequest extends HttpMessage { HostAndPort? hostAndPort; DateTime requestTime = DateTime.now(); //请求时间 HttpResponse? response; + Map attributes = {}; HttpRequest(this.method, this.uri, {String protocolVersion = "HTTP/1.1"}) : super(protocolVersion); @@ -176,7 +177,7 @@ enum ContentType { ///HTTP响应。 class HttpResponse extends HttpMessage { - final HttpStatus status; + HttpStatus status; DateTime responseTime = DateTime.now(); HttpRequest? request; @@ -283,7 +284,7 @@ class HttpStatus { return HttpStatus(statusCode, reasonPhrase); } - static HttpStatus? valueOf(int code) { + static HttpStatus valueOf(int code) { switch (code) { case 200: return ok; @@ -304,7 +305,7 @@ class HttpStatus { case 504: return gatewayTimeout; } - return null; + return HttpStatus(code, ""); } final int code; diff --git a/lib/network/http/http_headers.dart b/lib/network/http/http_headers.dart index 39a0e3e..3601a6c 100644 --- a/lib/network/http/http_headers.dart +++ b/lib/network/http/http_headers.dart @@ -128,6 +128,12 @@ class HttpHeaders { } } + //清空 + void clean() { + _headers.clear(); + _originalHeaderNames.clear(); + } + String headerLines() { StringBuffer sb = StringBuffer(); forEach((name, values) { @@ -148,6 +154,15 @@ class HttpHeaders { return json; } + ///转换json + Map toMap() { + Map json = {}; + forEach((name, values) { + json[name] = values.join(";"); + }); + return json; + } + ///从json解析 factory HttpHeaders.fromJson(Map json) { HttpHeaders headers = HttpHeaders(); diff --git a/lib/network/http_client.dart b/lib/network/http_client.dart index f0298fb..e2a537b 100644 --- a/lib/network/http_client.dart +++ b/lib/network/http_client.dart @@ -87,7 +87,7 @@ class HttpClients { /// 发送get请求 static Future get(String url, {Duration duration = const Duration(seconds: 3)}) async { HttpRequest msg = HttpRequest(HttpMethod.get, url); - return request(HostAndPort.of(url), msg); + return request(HostAndPort.of(url), msg, duration: duration); } /// 发送请求 @@ -106,7 +106,7 @@ class HttpClients { /// 发送代理请求 static Future proxyRequest(HttpRequest request, - {ProxyInfo? proxyInfo, Duration timeout = const Duration(seconds: 3)}) async { + {ProxyInfo? proxyInfo, Duration timeout = const Duration(seconds: 10)}) async { if (request.headers.host == null || request.headers.host?.trim().isEmpty == true) { try { request.headers.host = '${Uri.parse(request.uri).host}:${Uri.parse(request.uri).port}'; @@ -114,9 +114,7 @@ class HttpClients { } var httpResponseHandler = HttpResponseHandler(); - HostAndPort hostPort = HostAndPort.of(request.uri); - Channel channel = await proxyConnect(proxyInfo: proxyInfo, hostPort, httpResponseHandler); if (hostPort.isSsl()) { diff --git a/lib/network/network.dart b/lib/network/network.dart index 60c3fad..e1517d9 100644 --- a/lib/network/network.dart +++ b/lib/network/network.dart @@ -24,6 +24,7 @@ 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/host_filter.dart'; +import 'package:network_proxy/utils/platform.dart'; import 'host_port.dart'; @@ -61,7 +62,8 @@ class Network { HostAndPort? hostAndPort = channel.getAttribute(AttributeKeys.host); //黑名单 或 没开启https 直接转发 - if (HostFilter.filter(hostAndPort?.host) || (hostAndPort?.isSsl() == true && configuration?.enableSsl == false)) { + if ((Platforms.isMobile() && HostFilter.filter(hostAndPort?.host)) || + (hostAndPort?.isSsl() == true && configuration?.enableSsl == false)) { relay(channel, channel.getAttribute(channel.id)); channel.pipeline.channelRead(channel, data); return; diff --git a/lib/network/util/request_rewrite.dart b/lib/network/util/request_rewrite.dart index 97f7703..3c7d3dd 100644 --- a/lib/network/util/request_rewrite.dart +++ b/lib/network/util/request_rewrite.dart @@ -42,6 +42,7 @@ class RequestRewrites { return null; } + /// String? findResponseReplaceWith(String? domain, String? path) { if (!enabled || path == null) { return null; @@ -57,7 +58,7 @@ class RequestRewrites { return null; } - // + ///添加规则 void addRule(RequestRewriteRule rule) { rules.removeWhere((it) => it.path == rule.path && it.domain == rule.domain); rules.add(rule); diff --git a/lib/network/util/script_manager.dart b/lib/network/util/script_manager.dart new file mode 100644 index 0000000..77addfe --- /dev/null +++ b/lib/network/util/script_manager.dart @@ -0,0 +1,274 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter_js/flutter_js.dart'; +import 'package:network_proxy/network/http/http.dart'; +import 'package:path_provider/path_provider.dart'; + +/// @author wanghongen +/// 2023/10/06 +/// js脚本 +class ScriptManager { + static String separator = Platform.pathSeparator; + static ScriptManager? _instance; + bool enabled = true; + List list = []; + + final Map _scriptMap = {}; + + static JavascriptRuntime flutterJs = getJavascriptRuntime(); + + ScriptManager._(); + + ///单例 + static Future get instance async { + if (_instance == null) { + _instance = ScriptManager._(); + await _instance?.reloadScript(); + print('init script manager'); + } + return _instance!; + } + + ///重新加载脚本 + Future reloadScript() async { + List scripts = []; + var file = await _path; + print("reloadScript ${file.path}"); + if (await file.exists()) { + var content = await file.readAsString(); + if (content.isEmpty) { + return; + } + var config = jsonDecode(content); + enabled = config['enabled'] == true; + for (var entry in config['list']) { + scripts.add(ScriptItem.fromJson(entry)); + } + } + list = scripts; + _scriptMap.clear(); + } + + static String? _homePath; + + static Future homePath() async { + if (_homePath != null) { + return _homePath!; + } + + if (Platform.isMacOS) { + _homePath = await DesktopMultiWindow.invokeMethod(0, "getApplicationSupportDirectory"); + } else { + _homePath = await getApplicationSupportDirectory().then((it) => it.path); + } + return _homePath!; + } + + static Future get _path async { + final path = await homePath(); + var file = File('$path${separator}script.json'); + if (!await file.exists()) { + await file.create(); + } + return file; + } + + Future getScript(ScriptItem item) async { + if (_scriptMap.containsKey(item)) { + return _scriptMap[item]!; + } + var script = await File(item.scriptPath!).readAsString(); + _scriptMap[item] = script; + return script; + } + + ///添加脚本 + Future addScript(ScriptItem item, String script) async { + final path = await homePath(); + var file = File('$path${separator}scripts$separator${DateTime.now().millisecondsSinceEpoch}.js'); + await file.create(recursive: true); + file.writeAsString(script); + item.scriptPath = file.path; + list.add(item); + _scriptMap[item] = script; + } + + ///更新脚本 + Future updateScript(ScriptItem item, String script) async { + if (_scriptMap[item] == script) { + return; + } + + File(item.scriptPath!).writeAsString(script); + _scriptMap[item] = script; + } + + ///删除脚本 + Future removeScript(int index) async { + var item = list.removeAt(index); + File(item.scriptPath!).delete(); + flushConfig(); + } + + ///刷新配置 + Future flushConfig() async { + _path.then((value) => value.writeAsString(jsonEncode({'enabled': enabled, 'list': list}))); + } + + ///脚本上下文 + Map scriptContext(ScriptItem item) { + return { + 'scriptName': item.name, + 'os': Platform.operatingSystem, + }; + } + + ///运行脚本 + Future runScript(HttpRequest request) async { + if (!enabled) { + return request; + } + var url = request.requestUrl; + for (var item in list) { + if (item.enabled && item.match(url)) { + var context = jsonEncode(scriptContext(item)); + var jsRequest = jsonEncode(convertJsRequest(request)); + String script = await getScript(item); + + var jsResult = await flutterJs.evaluateAsync( + """var request = $jsRequest, context = $context; request['context'] = context; $script\n onRequest(context, request)"""); + var result = await jsResultResolve(jsResult); + request.attributes['scriptContext'] = result['context']; + return convertHttpRequest(request, result); + } + } + return request; + } + + ///运行脚本 + Future runResponseScript(HttpResponse response) async { + if (!enabled || response.request == null) { + return response; + } + + var request = response.request!; + var url = request.requestUrl; + for (var item in list) { + if (item.enabled && item.match(url)) { + var context = jsonEncode(request.attributes['scriptContext'] ?? scriptContext(item)); + var jsRequest = jsonEncode(convertJsRequest(request)); + var jsResponse = jsonEncode(convertJsResponse(response)); + print(context); + String script = await getScript(item); + var jsResult = await flutterJs.evaluateAsync("""$script\n onResponse($context, $jsRequest,$jsResponse);"""); + var result = await jsResultResolve(jsResult); + // print("response: ${jsResult.isPromise} ${jsResult.isError} $result"); + return convertHttpResponse(response, result); + } + } + return response; + } + + static Future jsResultResolve(JsEvalResult jsResult) async { + if (jsResult.isPromise) { + jsResult = await flutterJs.handlePromise(jsResult); + } + var result = jsResult.rawResult; + if (Platform.isMacOS) { + result = flutterJs.convertValue(jsResult); + } + if (result is Future) { + result = await (jsResult.rawResult as Future); + } + if (result is String) { + result = jsonDecode(result); + } + if (jsResult.isError) { + throw SignalException(jsResult.stringResult); + } + return result; + } + + //转换js request + Map convertJsRequest(HttpRequest request) { + var requestUri = request.requestUri; + return { + 'host': requestUri?.host, + 'url': request.requestUrl, + 'path': requestUri?.path, + 'queries': requestUri?.queryParameters, + 'headers': request.headers.toMap(), + 'method': request.method.name, + 'body': request.bodyAsString + }; + } + + //转换js response + Map convertJsResponse(HttpResponse response) { + return {'headers': response.headers.toMap(), 'statusCode': response.status.code, 'body': response.bodyAsString}; + } + + //http request + HttpRequest convertHttpRequest(HttpRequest request, Map map) { + request.headers.clean(); + request.method = HttpMethod.values.firstWhere((element) => element.name == map['method']); + String query = ''; + map['queries']?.forEach((key, value) { + query += '$key=$value&'; + }); + + request.uri = Uri.parse('${request.remoteDomain()}${map['path']}?$query').toString(); + + map['headers'].forEach((key, value) { + request.headers.add(key, value); + }); + request.body = map['body']?.toString().codeUnits; + return request; + } + + //http response + HttpResponse convertHttpResponse(HttpResponse response, Map map) { + response.headers.clean(); + response.status = HttpStatus.valueOf(map['statusCode']); + map['headers'].forEach((key, value) { + response.headers.add(key, value); + }); + response.body = map['body']?.toString().codeUnits; + return response; + } +} + +class ScriptItem { + bool enabled = true; + String? name; + String url; + String? scriptPath; + RegExp? urlReg; + + ScriptItem(this.enabled, this.name, this.url, {this.scriptPath}); + + //匹配url + bool match(String url) { + if (!this.url.startsWith('http://') && !this.url.startsWith('https://')) { + //不是http开头的url 需要去掉协议 + url = url.substring(url.indexOf('://') + 3); + } + urlReg ??= RegExp(this.url.replaceAll("*", ".*")); + return urlReg!.hasMatch(url); + } + + factory ScriptItem.fromJson(Map json) { + return ScriptItem(json['enabled'], json['name'], json['url'], scriptPath: json['scriptPath']); + } + + Map toJson() { + return {'enabled': enabled, 'name': name, 'url': url, 'scriptPath': scriptPath}; + } + + @override + String toString() { + return 'ScriptItem{enabled: $enabled, name: $name, url: $url, scriptPath: $scriptPath}'; + } +} diff --git a/lib/storage/favorites.dart b/lib/storage/favorites.dart index 94fa07e..53d3e20 100644 --- a/lib/storage/favorites.dart +++ b/lib/storage/favorites.dart @@ -8,6 +8,7 @@ import 'package:path_provider/path_provider.dart'; class FavoriteStorage { static Queue? _requests; + /// 获取收藏列表 static Future> get favorites async { if (_requests == null) { var file = await _path; diff --git a/lib/ui/component/multi_window.dart b/lib/ui/component/multi_window.dart index 175a75b..31e8926 100644 --- a/lib/ui/component/multi_window.dart +++ b/lib/ui/component/multi_window.dart @@ -2,11 +2,49 @@ import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; +import 'package:network_proxy/network/http/http.dart'; +import 'package:network_proxy/network/util/script_manager.dart'; import 'package:network_proxy/ui/component/encoder.dart'; +import 'package:network_proxy/ui/content/body.dart'; +import 'package:network_proxy/ui/desktop/left/request_editor.dart'; +import 'package:network_proxy/ui/desktop/toolbar/setting/script.dart'; import 'package:network_proxy/utils/platform.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:window_manager/window_manager.dart'; +///多窗口 +Widget multiWindow(int windowId, Map argument) { + if (argument['name'] == 'RequestEditor') { + return RequestEditor( + windowController: WindowController.fromWindowId(windowId), + request: argument['request'] == null ? null : HttpRequest.fromJson(argument['request'])); + } + //请求体 + if (argument['name'] == 'HttpBodyWidget') { + return HttpBodyWidget( + windowController: WindowController.fromWindowId(windowId), + httpMessage: HttpMessage.fromJson(argument['httpMessage']), + inNewWindow: true, + hideRequestRewrite: true); + } + //编码 + if (argument['name'] == 'EncoderWidget') { + return EncoderWidget( + type: EncoderType.nameOf(argument['type']), + text: argument['text'], + windowController: WindowController.fromWindowId(windowId)); + } + //脚本 + if (argument['name'] == 'ScriptWidget') { + return ScriptWidget(windowId: windowId); + } + + return const SizedBox(); +} + +//打开编码窗口 encodeWindow(EncoderType type, BuildContext context, [String? text]) async { if (Platforms.isMobile()) { Navigator.of(context).push(MaterialPageRoute(builder: (context) => EncoderWidget(type: type, text: text))); @@ -17,7 +55,6 @@ encodeWindow(EncoderType type, BuildContext context, [String? text]) async { if (Platform.isWindows) { ratio = WindowManager.instance.getDevicePixelRatio(); } - final window = await DesktopMultiWindow.createWindow(jsonEncode( {'name': 'EncoderWidget', 'type': type.name, 'text': text}, )); @@ -27,3 +64,50 @@ encodeWindow(EncoderType type, BuildContext context, [String? text]) async { ..center() ..show(); } + +bool _registerHandler = false; + +void methodHandler() { + if (_registerHandler) { + return; + } + _registerHandler = true; + DesktopMultiWindow.setMethodHandler((call, fromWindowId) async { + print('${call.method} ${call.arguments} $fromWindowId'); + + if (call.method == 'getApplicationSupportDirectory') { + return getApplicationSupportDirectory().then((it) => it.path); + } + if (call.method == 'getSaveLocation') { + return (await getSaveLocation(suggestedName: call.arguments))?.path; + } + if (call.method == 'openFile') { + XTypeGroup typeGroup = XTypeGroup(extensions: [call.arguments]); + final XFile? file = await openFile(acceptedTypeGroups: [typeGroup]); + return file?.path; + } + if (call.method == 'refreshScript') { + await ScriptManager.instance.then((value) { + return value.reloadScript(); + }); + } + return 'done'; + }); +} + +///打开脚本窗口 +openScriptWindow() async { + var ratio = 1.0; + if (Platform.isWindows) { + ratio = WindowManager.instance.getDevicePixelRatio(); + } + methodHandler(); + final window = await DesktopMultiWindow.createWindow(jsonEncode( + {'name': 'ScriptWidget'}, + )); + window.setTitle('脚本'); + window + ..setFrame(const Offset(30, 0) & Size(800 * ratio, 690 * ratio)) + ..center() + ..show(); +} diff --git a/lib/ui/component/widgets.dart b/lib/ui/component/widgets.dart index cd5e4a1..6a785ca 100644 --- a/lib/ui/component/widgets.dart +++ b/lib/ui/component/widgets.dart @@ -28,3 +28,52 @@ class _CustomPopupMenuItemState extends PopupMenuItemState onChanged; + + const SwitchWidget({super.key, this.title, this.subtitle, required this.value, required this.onChanged}); + + @override + State createState() => _SwitchState(); +} + +class _SwitchState extends State { + bool value = false; + + @override + void initState() { + super.initState(); + value = widget.value; + } + + @override + Widget build(BuildContext context) { + if (widget.title == null) { + return Switch( + value: value, + onChanged: (value) { + setState(() { + this.value = value; + }); + widget.onChanged(value); + }, + ); + } + return SwitchListTile( + title: widget.title == null ? null : Text(widget.title!), + subtitle: widget.subtitle == null ? null : Text(widget.subtitle!), + value: value, + dense: true, + onChanged: (value) { + setState(() { + this.value = value; + }); + widget.onChanged(value); + }, + ); + } +} diff --git a/lib/ui/desktop/left/history.dart b/lib/ui/desktop/left/history.dart index 8425c74..b7e27fe 100644 --- a/lib/ui/desktop/left/history.dart +++ b/lib/ui/desktop/left/history.dart @@ -11,6 +11,7 @@ import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/network/channel.dart'; import 'package:network_proxy/network/handler.dart'; import 'package:network_proxy/network/http/http.dart'; +import 'package:network_proxy/network/util/logger.dart'; import 'package:network_proxy/storage/histories.dart'; import 'package:network_proxy/ui/component/utils.dart'; import 'package:network_proxy/ui/component/widgets.dart'; @@ -98,7 +99,6 @@ class _HistoryState extends State<_HistoryWidget> { @override Widget build(BuildContext context) { - print("_HistoryState build"); List children = []; if (!_sessionSaved) { //当前会话未保存,是否保存当前会话 @@ -127,16 +127,12 @@ class _HistoryState extends State<_HistoryWidget> { //导入har import() async { - const XTypeGroup typeGroup = XTypeGroup( - label: 'Har', - extensions: ['har'], - ); + const XTypeGroup typeGroup = XTypeGroup(label: 'Har', extensions: ['har']); final XFile? file = await openFile(acceptedTypeGroups: [typeGroup]); if (file == null) { return; } - print(file); try { var historyItem = await storage.addHarFile(file); setState(() { @@ -144,8 +140,7 @@ class _HistoryState extends State<_HistoryWidget> { FlutterToastr.show("导入成功", context); }); } catch (e, t) { - print(e); - print(t); + logger.e('导入失败 $file', error: e, stackTrace: t); if (context.mounted) { FlutterToastr.show("导入失败 $e", context); } diff --git a/lib/ui/desktop/left/request_editor.dart b/lib/ui/desktop/left/request_editor.dart index dc7d866..90730df 100644 --- a/lib/ui/desktop/left/request_editor.dart +++ b/lib/ui/desktop/left/request_editor.dart @@ -1,3 +1,19 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; diff --git a/lib/ui/desktop/toolbar/setting/script.dart b/lib/ui/desktop/toolbar/setting/script.dart new file mode 100644 index 0000000..fb83b50 --- /dev/null +++ b/lib/ui/desktop/toolbar/setting/script.dart @@ -0,0 +1,463 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'dart:convert'; +import 'dart:io'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_code_editor/flutter_code_editor.dart'; +import 'package:flutter_highlight/themes/monokai-sublime.dart'; +import 'package:flutter_toastr/flutter_toastr.dart'; +import 'package:highlight/languages/javascript.dart'; +import 'package:network_proxy/network/util/logger.dart'; +import 'package:network_proxy/network/util/script_manager.dart'; +import 'package:network_proxy/ui/component/utils.dart'; +import 'package:network_proxy/ui/component/widgets.dart'; + +bool _refresh = false; + +/// 刷新脚本 +void _refreshScript() { + if (_refresh) { + return; + } + _refresh = true; + Future.delayed(const Duration(milliseconds: 1500), () async { + _refresh = false; + (await ScriptManager.instance).flushConfig(); + await DesktopMultiWindow.invokeMethod(0, "refreshScript"); + }); +} + +class ScriptWidget extends StatefulWidget { + final int windowId; + + const ScriptWidget({super.key, required this.windowId}); + + @override + State createState() => _ScriptWidgetState(); +} + +class _ScriptWidgetState extends State { + @override + void initState() { + super.initState(); + RawKeyboard.instance.addListener(onKeyEvent); + } + + @override + void dispose() { + RawKeyboard.instance.removeListener(onKeyEvent); + super.dispose(); + } + + void onKeyEvent(RawKeyEvent event) async { + if ((event.isKeyPressed(LogicalKeyboardKey.metaLeft) || event.isControlPressed) && + event.isKeyPressed(LogicalKeyboardKey.keyW)) { + RawKeyboard.instance.removeListener(onKeyEvent); + WindowController.fromWindowId(widget.windowId).close(); + return; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).dialogBackgroundColor, + appBar: AppBar( + title: const Text("脚本", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), + toolbarHeight: 36, + centerTitle: true), + body: Padding( + padding: const EdgeInsets.only(left: 15, right: 10), + child: futureWidget( + ScriptManager.instance, + loading: true, + (data) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row(children: [ + SizedBox( + width: 300, + child: SwitchWidget( + title: '启用脚本工具', + subtitle: "使用 JavaScript 修改请求和响应", + value: data.enabled, + onChanged: (value) { + data.enabled = value; + _refreshScript(); + }, + )), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const SizedBox(width: 10), + FilledButton( + style: ElevatedButton.styleFrom(padding: const EdgeInsets.only(left: 20, right: 20)), + onPressed: scriptEdit, + child: const Text("添加"), + ), + const SizedBox(width: 10), + OutlinedButton( + style: ElevatedButton.styleFrom(padding: const EdgeInsets.only(left: 20, right: 20)), + onPressed: import, + child: const Text("导入"), + ) + ], + )), + const SizedBox(width: 15) + ]), + const SizedBox(height: 5), + Container( + padding: const EdgeInsets.only(top: 10), + constraints: const BoxConstraints(maxHeight: 500, minHeight: 300), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.withOpacity(0.2)), + color: Colors.white, + backgroundBlendMode: BlendMode.colorBurn), + child: SingleChildScrollView( + child: Column(children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 200, padding: const EdgeInsets.only(left: 10), child: const Text("名称")), + const SizedBox(width: 50, child: Text("启用", textAlign: TextAlign.center)), + const VerticalDivider(), + const Expanded(child: Text("URL")), + ], + ), + const Divider(thickness: 0.5), + ScriptList(scripts: data.list, windowId: widget.windowId), + ]))), + ])))); + } + + //导入js + import() async { + String? file = Platform.isMacOS + ? await DesktopMultiWindow.invokeMethod(0, 'openFile', 'json') + : await openFile(acceptedTypeGroups: [ + const XTypeGroup(extensions: ['json']) + ]).then((it) => it?.path); + WindowController.fromWindowId(widget.windowId).show(); + if (file == null) { + return; + } + + try { + var json = jsonDecode(await File(file).readAsString()); + var scriptItem = ScriptItem.fromJson(json); + (await ScriptManager.instance).addScript(scriptItem, json['script']); + _refreshScript(); + if (context.mounted) { + FlutterToastr.show("导入成功", context); + } + setState(() {}); + } catch (e, t) { + logger.e('导入失败 $file', error: e, stackTrace: t); + if (context.mounted) { + FlutterToastr.show("导入失败 $e", context); + } + } + } + + /// 添加脚本 + scriptEdit() async { + showDialog(barrierDismissible: false, context: context, builder: (_) => const ScriptEdit()).then((value) { + if (value != null) { + setState(() {}); + } + }); + } +} + +/// 编辑脚本 +class ScriptEdit extends StatefulWidget { + final ScriptItem? scriptItem; + final String? script; + + const ScriptEdit({Key? key, this.scriptItem, this.script}) : super(key: key); + + @override + State createState() => _ScriptEditState(); +} + +class _ScriptEditState extends State { + static String template = """ +// 在请求到达服务器之前,调用此函数,您可以在此处修改请求数据 +// 例如Add/Update/Remove:Queries、Headers、Body +async function onRequest(context, request) { + console.log(request.url); + //URL参数 + //request.queries["name"] = "value"; + // 更新或添加新标头 + //request.headers["X-New-Headers"] = "My-Value"; + + //var body = JSON.parse(response.body); + //body['key'] = "value"; + //response.body = JSON.stringify(body); + return request; +} + +// 在将响应数据发送到客户端之前,调用此函数,您可以在此处修改响应数据 +async function onResponse(context, request, response) { + // 更新或添加新标头 + // response.headers["Name"] = "Value"; + // response.statusCode = 200; + + // Update Body + //response.body = await fetch('https://www.baidu.com/').then(response => response.text()); + return response; +} + """; + + late CodeController script; + late TextEditingController nameController; + late TextEditingController urlController; + + @override + void initState() { + super.initState(); + script = CodeController(language: javascript, text: widget.script ?? template); + nameController = TextEditingController(text: widget.scriptItem?.name); + urlController = TextEditingController(text: widget.scriptItem?.url); + } + + @override + void dispose() { + script.dispose(); + nameController.dispose(); + urlController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + GlobalKey formKey = GlobalKey(); + return AlertDialog( + scrollable: true, + titlePadding: const EdgeInsets.only(left: 15, top: 5, right: 15), + title: const Row(children: [ + Text("编辑脚本", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), + SizedBox(width: 10), + Tooltip(message: "使用 JavaScript 修改请求和响应", child: Icon(Icons.help_outline, size: 20)), + Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton())) + ]), + actionsPadding: const EdgeInsets.only(right: 10, bottom: 10), + actions: [ + ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: const Text("取消")), + FilledButton( + onPressed: () async { + if (!(formKey.currentState as FormState).validate()) { + FlutterToastr.show("名称和URL不能为空", context, position: FlutterToastr.top); + return; + } + //新增 + if (widget.scriptItem == null) { + var scriptItem = ScriptItem(true, nameController.text, urlController.text); + (await ScriptManager.instance).addScript(scriptItem, script.text); + } else { + widget.scriptItem?.name = nameController.text; + widget.scriptItem?.url = urlController.text; + (await ScriptManager.instance).updateScript(widget.scriptItem!, script.text); + } + + _refreshScript(); + if (context.mounted) { + Navigator.of(context).maybePop(true); + } + }, + child: const Text("保存")), + ], + content: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + textField("名称:", nameController, "请输入名称"), + const SizedBox(height: 10), + textField("URL:", urlController, "github.com/api/*", keyboardType: TextInputType.url), + const SizedBox(height: 10), + const Text("脚本:"), + const SizedBox(height: 5), + SizedBox( + width: 850, + height: 360, + child: CodeTheme( + data: CodeThemeData(styles: monokaiSublimeTheme), + child: SingleChildScrollView( + child: CodeField(textStyle: const TextStyle(fontSize: 13), controller: script)))) + ], + ))); + } + + Widget textField(String label, TextEditingController controller, String hint, {TextInputType? keyboardType}) { + return Row(children: [ + SizedBox(width: 50, child: Text(label)), + Expanded( + child: TextFormField( + controller: controller, + validator: (val) => val?.isNotEmpty == true ? null : "", + keyboardType: keyboardType, + decoration: InputDecoration( + hintText: hint, + errorStyle: const TextStyle(height: 0, fontSize: 0), + focusedBorder: focusedBorder(), + isDense: true, + constraints: const BoxConstraints(maxHeight: 35), + border: const OutlineInputBorder()), + )) + ]); + } + + InputBorder focusedBorder() { + return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2)); + } +} + +/// 脚本列表 +class ScriptList extends StatefulWidget { + final int windowId; + final List scripts; + + const ScriptList({Key? key, required this.scripts, required this.windowId}) : super(key: key); + + @override + State createState() => _ScriptListState(); +} + +class _ScriptListState extends State { + Map selected = {}; + + @override + Widget build(BuildContext context) { + return Column(children: rows(widget.scripts)); + } + + List rows(List list) { + var primaryColor = Theme.of(context).primaryColor; + + return List.generate(list.length, (index) { + return InkWell( + // onTap: () { + // selected[index] = !(selected[index] ?? false); + // setState(() {}); + // }, + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + hoverColor: primaryColor.withOpacity(0.3), + onDoubleTap: () async { + String script = await (await ScriptManager.instance).getScript(list[index]); + if (!context.mounted) { + return; + } + showDialog( + barrierDismissible: false, + context: context, + builder: (_) => ScriptEdit(scriptItem: list[index], script: script)).then((value) { + if (value != null) { + setState(() {}); + } + }); + }, + onSecondaryTapDown: (details) { + showContextMenu(context, details.globalPosition, items: [ + PopupMenuItem( + height: 35, + child: const Text("编辑"), + onTap: () { + showDialog( + barrierDismissible: false, + context: context, + builder: (_) => ScriptEdit(scriptItem: list[index])).then((value) { + if (value != null) { + setState(() {}); + } + }); + }), + PopupMenuItem(height: 35, child: const Text("导出"), onTap: () => export(list[index])), + PopupMenuItem( + height: 35, + child: list[index].enabled ? const Text("禁用") : const Text("启用"), + onTap: () { + list[index].enabled = !list[index].enabled; + _refreshScript(); + setState(() {}); + }), + const PopupMenuDivider(), + PopupMenuItem( + height: 35, + child: const Text("删除"), + onTap: () async { + (await ScriptManager.instance).removeScript(index); + setState(() {}); + if (context.mounted) FlutterToastr.show('删除成功', context); + }), + ]); + }, + child: Container( + color: selected[index] == true + ? primaryColor.withOpacity(0.8) + : index.isEven + ? Colors.grey.withOpacity(0.1) + : null, + height: 30, + padding: const EdgeInsets.all(5), + child: Row( + children: [ + SizedBox(width: 200, child: Text(list[index].name!, style: const TextStyle(fontSize: 13))), + SizedBox( + width: 40, + child: Transform.scale( + scale: 0.65, + child: SwitchWidget( + value: list[index].enabled, + onChanged: (val) { + list[index].enabled = val; + _refreshScript(); + }))), + const SizedBox(width: 20), + Expanded(child: Text(list[index].url, style: const TextStyle(fontSize: 13))), + ], + ))); + }); + } + + //导出js + export(ScriptItem item) async { + //文件名称 + String fileName = '${item.name}.json'; + String? saveLocation = Platform.isMacOS + ? await DesktopMultiWindow.invokeMethod(0, 'getSaveLocation', fileName) + : (await getSaveLocation(suggestedName: fileName))?.path; + WindowController.fromWindowId(widget.windowId).show(); + if (saveLocation == null) { + return; + } + var json = item.toJson(); + json.remove("scriptPath"); + json['script'] = await (await ScriptManager.instance).getScript(item); + final XFile xFile = XFile.fromData(utf8.encode(jsonEncode(json)), mimeType: 'json'); + await xFile.saveTo(saveLocation); + if (context.mounted) FlutterToastr.show("导出成功", context); + } +} diff --git a/lib/ui/desktop/toolbar/setting/setting.dart b/lib/ui/desktop/toolbar/setting/setting.dart index d8c721b..e525fc6 100644 --- a/lib/ui/desktop/toolbar/setting/setting.dart +++ b/lib/ui/desktop/toolbar/setting/setting.dart @@ -4,6 +4,7 @@ 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/util/system_proxy.dart'; +import 'package:network_proxy/ui/component/multi_window.dart'; import 'package:network_proxy/ui/desktop/toolbar/setting/external_proxy.dart'; import 'package:network_proxy/ui/desktop/toolbar/setting/request_rewrite.dart'; import 'package:network_proxy/ui/desktop/toolbar/setting/theme.dart'; @@ -59,9 +60,10 @@ class _SettingState extends State { }, menuChildren: [ _ProxyMenu(proxyServer: widget.proxyServer), + const ThemeSetting(), item("域名过滤", onPressed: hostFilter), item("请求重写", onPressed: requestRewrite), - const ThemeSetting(), + item("脚本", onPressed: () => openScriptWindow()), item("外部代理设置", onPressed: setExternalProxy), item("Github", onPressed: () => launchUrl(Uri.parse("https://github.com/wanghongenpin/network_proxy_flutter"))), ], @@ -181,7 +183,7 @@ class _ProxyMenuState extends State<_ProxyMenu> { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text("代理忽略域名"), + const Text("代理忽略域名", style: TextStyle(fontSize: 14)), const SizedBox(height: 3), Text("多个使用;分割", style: TextStyle(fontSize: 11, color: Colors.grey.shade600)), ], @@ -210,7 +212,7 @@ class _ProxyMenuState extends State<_ProxyMenu> { minLines: 1)), const SizedBox(height: 10), ], - child: const Padding(padding: EdgeInsets.only(left: 10), child: Text("代理")), + child: const Padding(padding: EdgeInsets.only(left: 10), child: Text("代理",style: TextStyle(fontSize: 14))), ); } diff --git a/lib/ui/desktop/toolbar/setting/theme.dart b/lib/ui/desktop/toolbar/setting/theme.dart index a41af28..35776e6 100644 --- a/lib/ui/desktop/toolbar/setting/theme.dart +++ b/lib/ui/desktop/toolbar/setting/theme.dart @@ -58,7 +58,7 @@ class ThemeSetting extends StatelessWidget { themeNotifier.value = themeNotifier.value.copy(mode: ThemeMode.light); }), ], - child: const Padding(padding: EdgeInsets.only(left: 10), child: Text("主题")), + child: const Padding(padding: EdgeInsets.only(left: 10), child: Text("主题",style: TextStyle(fontSize: 14))), ); } } diff --git a/lib/ui/launch/launch.dart b/lib/ui/launch/launch.dart index a71a232..ad86751 100644 --- a/lib/ui/launch/launch.dart +++ b/lib/ui/launch/launch.dart @@ -81,7 +81,7 @@ class _SocketLaunchState extends State with WindowListener, Widget color: started ? Colors.red : Colors.green, size: widget.size.toDouble()), onPressed: () async { if (started) { - if (widget.serverLaunch) { + if (!widget.serverLaunch) { setState(() { widget.onStop?.call(); started = !started; @@ -103,7 +103,7 @@ class _SocketLaunchState extends State with WindowListener, Widget ///启动代理服务器 start() { - if (widget.serverLaunch) { + if (!widget.serverLaunch) { setState(() { widget.onStart?.call(); started = true; diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index c7b52bd..b5fee3b 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_js_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterJsPlugin"); + flutter_js_plugin_register_with_registrar(flutter_js_registrar); g_autoptr(FlPluginRegistrar) proxy_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ProxyManagerPlugin"); proxy_manager_plugin_register_with_registrar(proxy_manager_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index fd6ce5f..4791c1f 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_multi_window file_selector_linux + flutter_js proxy_manager screen_retriever url_launcher_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 35cf907..dba7c8a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import desktop_multi_window import file_selector_macos +import flutter_js import path_provider_foundation import proxy_manager import screen_retriever @@ -17,6 +18,7 @@ import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterMultiWindowPlugin.register(with: registry.registrar(forPlugin: "FlutterMultiWindowPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterJsPlugin.register(with: registry.registrar(forPlugin: "FlutterJsPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ProxyManagerPlugin.register(with: registry.registrar(forPlugin: "ProxyManagerPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index 1ac8f69..8828718 100644 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -6,6 +6,12 @@ "filename" : "icon_16x16.png", "scale" : "1x" }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "icon_16x16@2x.png", + "scale" : "2x" + }, { "size" : "32x32", "idiom" : "mac", diff --git a/pubspec.lock b/pubspec.lock index ee7fb11..62f023c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.11.0" + autotrie: + dependency: transitive + description: + name: autotrie + sha256: "55da6faefb53cfcb0abb2f2ca8636123fb40e35286bb57440d2cf467568188f8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" basic_utils: dependency: "direct main" description: @@ -41,6 +49,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" clock: dependency: transitive description: @@ -69,10 +85,10 @@ packages: dependency: transitive description: name: cross_file - sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb + sha256: "445db18de832dba8d851e287aff8ccf169bed30d2e94243cb54c7d2f1ed2142c" url: "https://pub.flutter-io.cn" source: hosted - version: "0.3.3+5" + version: "0.3.3+6" crypto: dependency: "direct main" description: @@ -114,6 +130,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -215,6 +239,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" + flutter_code_editor: + dependency: "direct main" + description: + name: flutter_code_editor + sha256: "2e48e2a09c4205991787f299cd101f66f28e6845f882df543ae0f4260f9e2c67" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.9" + flutter_highlight: + dependency: transitive + description: + name: flutter_highlight + sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.0" + flutter_js: + dependency: "direct main" + description: + name: flutter_js + sha256: "5bf5db354fe78fe24cb90a5fa6b4423d38712440c88e3445c3dc88bc134c452f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.8.0" flutter_lints: dependency: "direct dev" description: @@ -249,6 +297,22 @@ packages: description: flutter source: sdk version: "0.0.0" + highlight: + dependency: transitive + description: + name: highlight + sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.0" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.3" http: dependency: transitive description: @@ -289,6 +353,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.8.1" + linked_scroll_controller: + dependency: transitive + description: + name: linked_scroll_controller + sha256: e6020062bcf4ffc907ee7fd090fa971e65d8dfaac3c62baf601a3ced0b37986a + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" lints: dependency: transitive description: @@ -301,10 +373,10 @@ packages: dependency: "direct main" description: name: logger - sha256: ba3bc83117b2b49bdd723c0ea7848e8285a0fbc597ba09203b20d329d020c24a + sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac" url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.2" + version: "2.0.2+1" logging: dependency: transitive description: @@ -405,10 +477,10 @@ packages: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.2" + version: "3.1.3" plugin_platform_interface: dependency: transitive description: @@ -465,22 +537,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.1.9" + scrollable_positioned_list: + dependency: transitive + description: + name: scrollable_positioned_list + sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.8" share_plus: dependency: "direct main" description: name: share_plus - sha256: "6cec740fa0943a826951223e76218df002804adb588235a8910dc3d6b0654e11" + sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd url: "https://pub.flutter-io.cn" source: hosted - version: "7.1.0" + version: "7.2.1" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "357412af4178d8e11d14f41723f80f12caea54cf0d5cd29af9dcdab85d58aea7" + sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 url: "https://pub.flutter-io.cn" source: hosted - version: "3.3.0" + version: "3.3.1" sky_engine: dependency: transitive description: flutter @@ -494,6 +574,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -518,6 +606,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.1" term_glyph: dependency: transitive description: @@ -534,6 +630,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.6.1" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: @@ -610,10 +714,10 @@ packages: dependency: transitive description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: b715b8d3858b6fa9f68f87d20d98830283628014750c2b09b6f516c1da4af2a7 url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.7" + version: "4.1.0" vector_math: dependency: transitive description: @@ -634,18 +738,18 @@ packages: dependency: transitive description: name: win32 - sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" url: "https://pub.flutter-io.cn" source: hosted - version: "5.0.7" + version: "5.0.9" window_manager: dependency: "direct main" description: name: window_manager - sha256: "6ee795be9124f90660ea9d05e581a466de19e1c89ee74fc4bf528f60c8600edd" + sha256: dcc865277f26a7dad263a47d0e405d77e21f12cb71f30333a52710a408690bd7 url: "https://pub.flutter-io.cn" source: hosted - version: "0.3.6" + version: "0.3.7" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f0fd54a..459ede8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: basic_utils: ^5.6.1 logger: ^2.0.1 date_format: ^2.0.7 - window_manager: ^0.3.6 + window_manager: ^0.3.7 desktop_multi_window: git: url: https://gitee.com/wanghongenpin/flutter-plugins.git @@ -28,10 +28,12 @@ dependencies: qrscan: ^0.3.3 flutter_barcode_scanner: ^2.0.0 flutter_toastr: ^1.0.3 - share_plus: ^7.1.0 + share_plus: ^7.2.1 brotli: ^0.6.0 installed_apps: ^1.3.1 file_selector: ^1.0.1 + flutter_js: ^0.8.0 + flutter_code_editor: dev_dependencies: flutter_test: sdk: flutter diff --git a/test/js_test.dart b/test/js_test.dart new file mode 100644 index 0000000..0eb716f --- /dev/null +++ b/test/js_test.dart @@ -0,0 +1,34 @@ +import 'dart:convert'; + +import 'package:flutter_js/flutter_js.dart'; +import 'package:network_proxy/network/http/http.dart'; + +//转换js request +Map convertJsRequest(HttpRequest request) { + return { + 'url': request.requestUrl, + 'path': request.path(), + 'headers': request.headers.toMap(), + 'method': request.method.name, + 'body': request.bodyAsString + }; +} + +main() { + var flutterJs = getJavascriptRuntime(); + var httpRequest = HttpRequest(HttpMethod.get, "https://www.v2ex.com"); + httpRequest.headers.set('user-agent', 'Dart/3.0 (dart:io)'); + + const code = """ + function httpRequest(request) { + console.log(request); + request.headers['heelo']='world'; + request.url = 'https://www.baidu.com'; + return null; + } + """; + var jsRequest = jsonEncode(convertJsRequest(httpRequest)); + + var evaluate = flutterJs.evaluate("""$code\n httpRequest($jsRequest);"""); + print(flutterJs.convertValue(evaluate)); +} diff --git a/test/tests.dart b/test/tests.dart index b6fd4b7..739c0d3 100644 --- a/test/tests.dart +++ b/test/tests.dart @@ -1,14 +1,11 @@ import 'dart:io'; void main() { - var iso8601string = DateTime.now().toUtc().toIso8601String(); - var parse = DateTime.parse(iso8601string).toLocal(); - print(DateTime.now().toIso8601String()); - print(parse.hour); - print(DateTime.parse(iso8601string)); - print(DateTime.now().toUtc().toIso8601String()); print(Platform.version); print(Platform.localHostname); print(Platform.operatingSystem); print(Platform.localeName); + print(Platform.script); + print(Platform.environment); + print(Platform.packageConfig); } diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 49a9901..41f8a63 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -19,6 +20,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("DesktopMultiWindowPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterJsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterJsPlugin")); ProxyManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ProxyManagerPlugin")); ScreenRetrieverPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index df44e67..c64635a 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_multi_window file_selector_windows + flutter_js proxy_manager screen_retriever share_plus