桌面端增加JS脚本

This commit is contained in:
wanghongen
2023-10-08 21:46:23 +08:00
parent ee82a6afac
commit d6504c6bb0
28 changed files with 1152 additions and 98 deletions

View File

@@ -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<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
var instance = UIConfiguration.instance;
//多窗口
if (args.firstOrNull == 'multi_window') {
@@ -28,6 +24,7 @@ void main(List<String> 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<String> args) async {
runApp(FluentApp(DesktopHomePage(configuration: await configuration), uiConfiguration: uiConfiguration));
}
///多窗口
Widget multiWindow(int windowId, Map<dynamic, dynamic> 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<ThemeModel> themeNotifier;
class FluentApp extends StatelessWidget {

View File

@@ -85,7 +85,7 @@ class Channel {
Future<void> 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;
}

View File

@@ -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<HttpRequest> {
//实现抓包代理转发
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<HttpRequest> {
}
//替换请求体
_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<HttpRequest> {
/// 异常处理
_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<HttpRequest> {
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<HttpRequest> {
..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<HttpResponse> {
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<Object> {
RelayHandler(this.remoteChannel);
@override
void channelRead(Channel channel, Object msg) {
void channelRead(Channel channel, Object msg) async {
//发送给客户端
remoteChannel.write(msg);
}

View File

@@ -87,6 +87,7 @@ class HttpRequest extends HttpMessage {
HostAndPort? hostAndPort;
DateTime requestTime = DateTime.now(); //请求时间
HttpResponse? response;
Map<String, dynamic> 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;

View File

@@ -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<String, String> toMap() {
Map<String, String> json = {};
forEach((name, values) {
json[name] = values.join(";");
});
return json;
}
///从json解析
factory HttpHeaders.fromJson(Map<String, dynamic> json) {
HttpHeaders headers = HttpHeaders();

View File

@@ -87,7 +87,7 @@ class HttpClients {
/// 发送get请求
static Future<HttpResponse> 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<HttpResponse> 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()) {

View File

@@ -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;

View File

@@ -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);

View File

@@ -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<ScriptItem> list = [];
final Map<ScriptItem, String> _scriptMap = {};
static JavascriptRuntime flutterJs = getJavascriptRuntime();
ScriptManager._();
///单例
static Future<ScriptManager> get instance async {
if (_instance == null) {
_instance = ScriptManager._();
await _instance?.reloadScript();
print('init script manager');
}
return _instance!;
}
///重新加载脚本
Future<void> reloadScript() async {
List<ScriptItem> 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<String> 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<File> get _path async {
final path = await homePath();
var file = File('$path${separator}script.json');
if (!await file.exists()) {
await file.create();
}
return file;
}
Future<String> getScript(ScriptItem item) async {
if (_scriptMap.containsKey(item)) {
return _scriptMap[item]!;
}
var script = await File(item.scriptPath!).readAsString();
_scriptMap[item] = script;
return script;
}
///添加脚本
Future<void> 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<void> updateScript(ScriptItem item, String script) async {
if (_scriptMap[item] == script) {
return;
}
File(item.scriptPath!).writeAsString(script);
_scriptMap[item] = script;
}
///删除脚本
Future<void> removeScript(int index) async {
var item = list.removeAt(index);
File(item.scriptPath!).delete();
flushConfig();
}
///刷新配置
Future<void> flushConfig() async {
_path.then((value) => value.writeAsString(jsonEncode({'enabled': enabled, 'list': list})));
}
///脚本上下文
Map<String, dynamic> scriptContext(ScriptItem item) {
return {
'scriptName': item.name,
'os': Platform.operatingSystem,
};
}
///运行脚本
Future<HttpRequest> 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<HttpResponse> 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<dynamic> 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<String, dynamic> 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<String, dynamic> convertJsResponse(HttpResponse response) {
return {'headers': response.headers.toMap(), 'statusCode': response.status.code, 'body': response.bodyAsString};
}
//http request
HttpRequest convertHttpRequest(HttpRequest request, Map<dynamic, dynamic> 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<dynamic, dynamic> 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<String, dynamic> json) {
return ScriptItem(json['enabled'], json['name'], json['url'], scriptPath: json['scriptPath']);
}
Map<String, dynamic> toJson() {
return {'enabled': enabled, 'name': name, 'url': url, 'scriptPath': scriptPath};
}
@override
String toString() {
return 'ScriptItem{enabled: $enabled, name: $name, url: $url, scriptPath: $scriptPath}';
}
}

View File

@@ -8,6 +8,7 @@ import 'package:path_provider/path_provider.dart';
class FavoriteStorage {
static Queue<HttpRequest>? _requests;
/// 获取收藏列表
static Future<Queue<HttpRequest>> get favorites async {
if (_requests == null) {
var file = await _path;

View File

@@ -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<dynamic, dynamic> 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: <String>[call.arguments]);
final XFile? file = await openFile(acceptedTypeGroups: <XTypeGroup>[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();
}

View File

@@ -28,3 +28,52 @@ class _CustomPopupMenuItemState<T> extends PopupMenuItemState<T, CustomPopupMenu
);
}
}
class SwitchWidget extends StatefulWidget {
final String? title;
final String? subtitle;
final bool value;
final ValueChanged<bool> onChanged;
const SwitchWidget({super.key, this.title, this.subtitle, required this.value, required this.onChanged});
@override
State<StatefulWidget> createState() => _SwitchState();
}
class _SwitchState extends State<SwitchWidget> {
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);
},
);
}
}

View File

@@ -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<Widget> children = [];
if (!_sessionSaved) {
//当前会话未保存,是否保存当前会话
@@ -127,16 +127,12 @@ class _HistoryState extends State<_HistoryWidget> {
//导入har
import() async {
const XTypeGroup typeGroup = XTypeGroup(
label: 'Har',
extensions: <String>['har'],
);
const XTypeGroup typeGroup = XTypeGroup(label: 'Har', extensions: <String>['har']);
final XFile? file = await openFile(acceptedTypeGroups: <XTypeGroup>[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);
}

View File

@@ -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';

View File

@@ -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<ScriptWidget> createState() => _ScriptWidgetState();
}
class _ScriptWidgetState extends State<ScriptWidget> {
@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<StatefulWidget> createState() => _ScriptEditState();
}
class _ScriptEditState extends State<ScriptEdit> {
static String template = """
// 在请求到达服务器之前,调用此函数,您可以在此处修改请求数据
// 例如Add/Update/RemoveQueries、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<FormState>();
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<ScriptItem> scripts;
const ScriptList({Key? key, required this.scripts, required this.windowId}) : super(key: key);
@override
State<ScriptList> createState() => _ScriptListState();
}
class _ScriptListState extends State<ScriptList> {
Map<int, bool> selected = {};
@override
Widget build(BuildContext context) {
return Column(children: rows(widget.scripts));
}
List<Widget> rows(List<ScriptItem> 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);
}
}

View File

@@ -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<Setting> {
},
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))),
);
}

View File

@@ -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))),
);
}
}

View File

@@ -81,7 +81,7 @@ class _SocketLaunchState extends State<SocketLaunch> 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<SocketLaunch> with WindowListener, Widget
///启动代理服务器
start() {
if (widget.serverLaunch) {
if (!widget.serverLaunch) {
setState(() {
widget.onStart?.call();
started = true;

View File

@@ -8,6 +8,7 @@
#include <desktop_multi_window/desktop_multi_window_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_js/flutter_js_plugin.h>
#include <proxy_manager/proxy_manager_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
@@ -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);

View File

@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
desktop_multi_window
file_selector_linux
flutter_js
proxy_manager
screen_retriever
url_launcher_linux

View File

@@ -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"))

View File

@@ -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",

View File

@@ -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:

View File

@@ -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

34
test/js_test.dart Normal file
View File

@@ -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<String, dynamic> 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));
}

View File

@@ -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);
}

View File

@@ -8,6 +8,7 @@
#include <desktop_multi_window/desktop_multi_window_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_js/flutter_js_plugin.h>
#include <proxy_manager/proxy_manager_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
@@ -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(

View File

@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
desktop_multi_window
file_selector_windows
flutter_js
proxy_manager
screen_retriever
share_plus