mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-04-27 22:49:53 +08:00
464 lines
17 KiB
Dart
464 lines
17 KiB
Dart
/*
|
||
* 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/gestures.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 = await DesktopMultiWindow.invokeMethod(0, 'openFile', 'json');
|
||
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/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";
|
||
|
||
// Update Body 使用fetch API请求接口,具体文档可网上搜索fetch API
|
||
//response.body = await fetch('https://www.baidu.com/').then(response => response.text());
|
||
return request;
|
||
}
|
||
|
||
// 在将响应数据发送到客户端之前,调用此函数,您可以在此处修改响应数据
|
||
async function onResponse(context, request, response) {
|
||
// 更新或添加新标头
|
||
// response.headers["Name"] = "Value";
|
||
// response.statusCode = 200;
|
||
|
||
//var body = JSON.parse(response.body);
|
||
//body['key'] = "value";
|
||
//response.body = JSON.stringify(body);
|
||
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: Row(children: [
|
||
const Text("编辑脚本", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||
const SizedBox(width: 10),
|
||
Text.rich(TextSpan(
|
||
text: '使用文档',
|
||
style: const TextStyle(color: Colors.blue, fontSize: 14),
|
||
recognizer: TapGestureRecognizer()
|
||
..onTap = () => DesktopMultiWindow.invokeMethod(
|
||
0, "launchUrl", 'https://gitee.com/wanghongenpin/network-proxy-flutter/wikis/%E8%84%9A%E6%9C%AC'))),
|
||
const 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;
|
||
setState(() {});
|
||
}),
|
||
const PopupMenuDivider(),
|
||
PopupMenuItem(
|
||
height: 35,
|
||
child: const Text("删除"),
|
||
onTap: () async {
|
||
(await ScriptManager.instance).removeScript(index);
|
||
_refreshScript();
|
||
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 = await DesktopMultiWindow.invokeMethod(0, 'getSaveLocation', fileName);
|
||
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);
|
||
}
|
||
}
|