diff --git a/lib/network/components/interceptor.dart b/lib/network/components/interceptor.dart index a80ae4a..53b112b 100644 --- a/lib/network/components/interceptor.dart +++ b/lib/network/components/interceptor.dart @@ -11,6 +11,11 @@ abstract class Interceptor { return hostAndPort; } + /// Called before the request is sent to the server. + Future execute(HttpRequest request) async { + return null; + } + /// Called before the request is sent to the server. Future onRequest(HttpRequest request) async { return request; diff --git a/lib/network/components/manager/request_map_manager.dart b/lib/network/components/manager/request_map_manager.dart new file mode 100644 index 0000000..948d8d3 --- /dev/null +++ b/lib/network/components/manager/request_map_manager.dart @@ -0,0 +1,264 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:path_provider/path_provider.dart'; + +import '../../util/logger.dart'; +import '../../util/random.dart'; + +class RequestMapManager { + static RequestMapManager? _instance; + + static String separator = Platform.pathSeparator; + + RequestMapManager._internal(); + + final Map _mapItemsCache = {}; + + bool enabled = true; + + //存储所有的请求映射规则 + List rules = []; + + ///单例 + static Future get instance async { + if (_instance == null) { + _instance = RequestMapManager._internal(); + await _instance?.reloadConfig(); + } + return _instance!; + } + + //添加规则 + Future addRule(RequestMapRule rule, RequestMapItem item) async { + final path = await homePath(); + String itemPath = "${separator}request_map$separator${RandomUtil.randomString(16)}.json"; + var file = File(path + itemPath); + await file.create(recursive: true); + final itemJson = jsonEncode(item.toJson()); + file.writeAsString(itemJson); + + rule.itemPath = itemPath; + _mapItemsCache[rule] = item; + rules.add(rule); + + await flushConfig(); + } + + //update rule + Future updateRule(RequestMapRule rule, RequestMapItem item) async { + rule.updatePathReg(); + if (rule.itemPath != null) { + final path = await homePath(); + var file = File('$path${rule.itemPath}'); + await file.writeAsString(jsonEncode(item.toJson())); + } + _mapItemsCache[rule] = item; + await flushConfig(); + } + + //删除规则 + Future deleteRule(int index) async { + var item = rules.removeAt(index); + final home = await homePath(); + File(home + item.itemPath!).delete(); + } + + //根据url和类型查找匹配的规则 + RequestMapRule? findMatch(String url) { + for (var rule in rules) { + if (rule.match(url)) { + return rule; + } + } + return null; + } + + Future getMapItem(RequestMapRule rule) async { + if (_mapItemsCache.containsKey(rule)) { + return _mapItemsCache[rule]; + } + + if (rule.itemPath != null) { + final path = await homePath(); + var file = File('$path$separator${rule.itemPath}'); + if (await file.exists()) { + var content = await file.readAsString(); + if (content.isNotEmpty) { + var item = RequestMapItem.fromJson(jsonDecode(content)); + _mapItemsCache[rule] = item; + return item; + } + } + } + return null; + } + + 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${Platform.pathSeparator}request_map.json'); + if (!await file.exists()) { + await file.create(); + } + return file; + } + + ///重新加载配置 + Future reloadConfig() async { + List list = []; + var file = await _path; + logger.d("reload request map config from ${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']) { + list.add(RequestMapRule.fromJson(entry)); + } + } + rules = list; + _mapItemsCache.clear(); + } + + ///保存配置 + Future flushConfig() async { + var file = await _path; + if (!await file.exists()) { + await file.create(recursive: true); + } + + var config = { + 'enabled': enabled, + 'list': rules.map((e) => e.toJson()).toList(), + }; + + await file.writeAsString(jsonEncode(config)); + } +} + +enum RequestMapType { + local("本地"), + script("脚本"), + ; + + //名称 + final String label; + + const RequestMapType(this.label); + + static RequestMapType fromName(String name) { + return values.firstWhere((element) => element.name == name || element.label == name); + } +} + +class RequestMapRule { + bool enabled; + RequestMapType type; + + String? name; + String url; + RegExp _urlReg; + String? itemPath; + + RequestMapRule({this.enabled = true, this.name, required this.url, required this.type, this.itemPath}) + : _urlReg = RegExp(url.replaceAll("*", ".*").replaceFirst('?', '\\?')); + + bool match(String url) { + if (enabled) { + return _urlReg.hasMatch(url); + } + return false; + } + + /// 从json中创建 + factory RequestMapRule.fromJson(Map map) { + return RequestMapRule( + enabled: map['enabled'] == true, + name: map['name'], + url: map['url'], + type: RequestMapType.fromName(map['type']), + itemPath: map['itemPath']); + } + + void updatePathReg() { + _urlReg = RegExp(url.replaceAll("*", ".*").replaceFirst('?', '\\?')); + } + + Map toJson() { + return { + 'name': name, + 'enabled': enabled, + 'url': url, + 'type': type.name, + 'itemPath': itemPath, + }; + } +} + +class RequestMapItem { + String? script; + + int? statusCode; + Map? headers; + + //body + String? body; + + String? bodyType; + + String? bodyFile; + + RequestMapItem({this.script, this.statusCode, this.headers, this.body, this.bodyType, this.bodyFile}); + + /// 从json中创建 + factory RequestMapItem.fromJson(Map map) { + return RequestMapItem( + script: map['script'], + statusCode: map['statusCode'], + headers: (map['headers'] as Map?)?.cast(), + body: map['body'], + bodyType: map['bodyType'], + bodyFile: map['bodyFile'], + ); + } + + Map toJson() { + return { + 'script': script, + 'statusCode': statusCode, + 'headers': headers, + 'body': body, + 'bodyType': bodyType, + 'bodyFile': bodyFile, + }; + } +} + +enum MapBodyType { + text("文本"), + file("文件"); + + final String label; + + const MapBodyType(this.label); +} diff --git a/lib/network/components/manager/request_rewrite_manager.dart b/lib/network/components/manager/request_rewrite_manager.dart index 982f420..e388b6b 100644 --- a/lib/network/components/manager/request_rewrite_manager.dart +++ b/lib/network/components/manager/request_rewrite_manager.dart @@ -59,63 +59,13 @@ class RequestRewriteManager { enabled = map['enabled'] == true; List list = map['rules'] ?? []; rules.clear(); - bool flush = false; for (var element in list) { try { - bool oldVersion = false; - // body("重写消息体"), 兼容旧版本 - if (element['requestBody']?.isNotEmpty == true || element['queryParam']?.isNotEmpty == true) { - element['type'] = RuleType.requestReplace.name; - - List items = []; - if (element['requestBody']?.isNotEmpty == true) { - RewriteItem item = RewriteItem(RewriteType.replaceRequestBody, true); - item.body = element['requestBody']; - items.add(item); - } - if (element['queryParam']?.isNotEmpty == true) { - RewriteItem item = RewriteItem(RewriteType.replaceRequestLine, true); - item.queryParam = element['queryParam']; - items.add(item); - } - var rule = RequestRewriteRule.formJson(element); - await addRule(rule, items); - oldVersion = true; - } - - if (element['responseBody']?.isNotEmpty == true) { - element['type'] = RuleType.responseReplace.name; - RewriteItem item = RewriteItem(RewriteType.replaceResponseBody, true); - item.body = element['responseBody']; - var rule = RequestRewriteRule.formJson(element); - await addRule(rule, [item]); - - oldVersion = true; - continue; - } - - if (element['redirectUrl']?.isNotEmpty == true) { - RewriteItem item = RewriteItem(RewriteType.redirect, true); - item.redirectUrl = element['redirectUrl']; - var rule = RequestRewriteRule.formJson(element); - await addRule(rule, [item]); - oldVersion = true; - continue; - } - - if (oldVersion) { - flush = true; - continue; - } rules.add(RequestRewriteRule.formJson(element)); } catch (e) { logger.e('加载请求重写配置失败 $element', error: e); } } - - if (flush) { - await flushRequestRewriteConfig(); - } } ///重新加载请求重写 @@ -274,7 +224,7 @@ class RequestRewriteManager { return items; } - toJson() { + Map toJson() { return { 'enabled': enabled, 'rules': rules.map((e) => e.toJson()).toList(), diff --git a/lib/network/components/request_map.dart b/lib/network/components/request_map.dart new file mode 100644 index 0000000..a6c5120 --- /dev/null +++ b/lib/network/components/request_map.dart @@ -0,0 +1,79 @@ +/* + * Copyright 2025 Hongen Wang All rights reserved. + * + * 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 'package:proxypin/network/components/interceptor.dart'; +import 'package:proxypin/network/http/http.dart'; +import 'package:proxypin/network/util/file_read.dart'; + +import 'manager/request_map_manager.dart'; + +/// RequestRewriteComponent is a component that can rewrite the request before sending it to the server. +/// @author Hongen Wang +class RequestMapInterceptor extends Interceptor { + static RequestMapInterceptor instance = RequestMapInterceptor._(); + + final managerInstance = RequestMapManager.instance; + + RequestMapInterceptor._(); + + @override + Future execute(HttpRequest request) async { + final manager = await managerInstance; + if (!manager.enabled) { + return null; + } + + return null; + } + + /// 重写响应 + Future mapLocalResponse(String url, HttpResponse response) async { + final manager = await RequestMapManager.instance; + var mapRule = manager.findMatch(url); + if (mapRule == null) { + return; + } + + if (mapRule.type == RequestMapType.script) { + // var rewriteItems = await manager.getMapItem(rewriteRule); + // await _replaceResponse(response, item); + } + } + + //替换相应 + Future _replaceResponse(RequestMapRule rule, RequestMapItem item) async { + // if (rule.type == RequestMapType.script) { + // response.status = HttpStatus.valueOf(item.statusCode!); + // return; + // } + + HttpResponse response = HttpResponse(HttpStatus.valueOf(item.statusCode ?? 200)); + item.headers?.forEach((key, value) { + response.headers.set(key, value); + }); + + if (item.bodyType == MapBodyType.file.name) { + if (item.bodyFile == null) return response; + response.body = await FileRead.readFile(item.bodyFile!); + } else if (item.body != null) { + response.body = + response.charset == 'utf-8' || response.charset == 'utf8' ? utf8.encode(item.body!) : item.body?.codeUnits; + } + return response; + } +} diff --git a/lib/ui/component/multi_window.dart b/lib/ui/component/multi_window.dart index 3207f03..80021f6 100644 --- a/lib/ui/component/multi_window.dart +++ b/lib/ui/component/multi_window.dart @@ -23,6 +23,7 @@ import 'package:flutter/material.dart'; import 'package:proxypin/l10n/app_localizations.dart'; import 'package:path_provider/path_provider.dart'; import 'package:proxypin/network/bin/server.dart'; +import 'package:proxypin/network/components/manager/request_map_manager.dart'; import 'package:proxypin/network/components/manager/request_rewrite_manager.dart'; import 'package:proxypin/network/components/manager/rewrite_rule.dart'; import 'package:proxypin/network/components/manager/script_manager.dart'; @@ -208,6 +209,10 @@ void registerMethodHandler() { if (call.method == 'getProxyInfo') { return ProxyServer.current?.isRunning == true ? {'host': '127.0.0.1', 'port': ProxyServer.current!.port} : null; } + if (call.method == 'refreshRequestRewrite') { + await MultiWindow._handleRefreshRewrite(Operation.of(call.arguments['operation']), call.arguments); + return 'done'; + } if (call.method == 'refreshScript') { await ScriptManager.instance.then((value) { @@ -216,8 +221,10 @@ void registerMethodHandler() { return 'done'; } - if (call.method == 'refreshRequestRewrite') { - await MultiWindow._handleRefreshRewrite(Operation.of(call.arguments['operation']), call.arguments); + if (call.method == 'refreshRequestMap') { + await RequestMapManager.instance.then((value) { + return value.reloadConfig(); + }); return 'done'; } @@ -257,7 +264,7 @@ void registerMethodHandler() { } ///打开编码窗口 -encodeWindow(EncoderType type, BuildContext context, [String? text]) async { +Future encodeWindow(EncoderType type, BuildContext context, [String? text]) async { if (Platforms.isMobile()) { Navigator.of(context).push(MaterialPageRoute(builder: (context) => EncoderWidget(type: type, text: text))); return; @@ -278,7 +285,7 @@ encodeWindow(EncoderType type, BuildContext context, [String? text]) async { ..show(); } -openScriptConsoleWindow() async { +Future openScriptConsoleWindow() async { var ratio = 1.0; if (Platform.isWindows) { ratio = WindowManager.instance.getDevicePixelRatio(); diff --git a/lib/ui/desktop/request/repeat.dart b/lib/ui/desktop/request/repeat.dart index 2c6e572..5e4a4c7 100644 --- a/lib/ui/desktop/request/repeat.dart +++ b/lib/ui/desktop/request/repeat.dart @@ -97,7 +97,7 @@ class _CustomRepeatState extends State { //Checkbox样式 固定和随机 Row(children: [ SizedBox( - width: isEN ? 100 : 82, + width: isEN ? 107 : 84, height: 35, child: Transform.scale( scale: 0.83, @@ -116,7 +116,7 @@ class _CustomRepeatState extends State { ]), Row(children: [ SizedBox( - width: isEN ? 100 : 82, + width: isEN ? 107 : 84, height: 35, child: Transform.scale( scale: 0.83, diff --git a/lib/ui/desktop/toolbar/setting/request_map.dart b/lib/ui/desktop/toolbar/setting/request_map.dart new file mode 100644 index 0000000..0eb36b6 --- /dev/null +++ b/lib/ui/desktop/toolbar/setting/request_map.dart @@ -0,0 +1,607 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_toastr/flutter_toastr.dart'; +import 'package:proxypin/l10n/app_localizations.dart'; +import 'package:proxypin/network/components/manager/request_map_manager.dart'; +import 'package:proxypin/network/components/manager/script_manager.dart'; +import 'package:proxypin/ui/component/app_dialog.dart'; +import 'package:proxypin/ui/component/utils.dart'; +import 'package:proxypin/ui/component/widgets.dart'; +import 'package:proxypin/ui/desktop/toolbar/setting/request_map/map_local.dart'; +import 'package:proxypin/ui/desktop/toolbar/setting/request_map/map_scipt.dart'; + +import '../../../../network/util/logger.dart'; + +bool _refresh = false; + +/// 刷新配置 +void _refreshConfig({bool force = false}) { + if (_refresh && !force) { + return; + } + _refresh = true; + Future.delayed(const Duration(milliseconds: 1500), () async { + _refresh = false; + await RequestMapManager.instance.then((manager) => manager.flushConfig()); + await DesktopMultiWindow.invokeMethod(0, "refreshRequestMap"); + }); +} + +class RequestMapPage extends StatefulWidget { + final int? windowId; + + const RequestMapPage({super.key, this.windowId}); + + @override + State createState() => _RequestMapPageState(); +} + +class _RequestMapPageState extends State { + AppLocalizations get localizations => AppLocalizations.of(context)!; + + @override + void initState() { + super.initState(); + HardwareKeyboard.instance.addHandler(onKeyEvent); + } + + @override + void dispose() { + HardwareKeyboard.instance.removeHandler(onKeyEvent); + super.dispose(); + } + + bool onKeyEvent(KeyEvent event) { + if (HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.escape) && Navigator.canPop(context)) { + Navigator.maybePop(context); + return true; + } + + if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) && + event.logicalKey == LogicalKeyboardKey.keyW) { + HardwareKeyboard.instance.removeHandler(onKeyEvent); + WindowController.fromWindowId(widget.windowId!).close(); + return true; + } + return false; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("请求映射", style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), + toolbarHeight: 36, + centerTitle: true), + body: Padding( + padding: const EdgeInsets.only(left: 15, right: 10), + child: futureWidget( + RequestMapManager.instance, + loading: true, + (data) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row(children: [ + SizedBox( + width: 350, + child: ListTile( + title: Text("启用请求映射"), + subtitle: Text("不请求远程服务,使用本地配置或脚本进行响应"), + trailing: SwitchWidget( + value: data.enabled, + scale: 0.8, + onChanged: (value) { + data.enabled = value; + // _refreshScript(); + }))), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const SizedBox(width: 10), + TextButton.icon( + icon: const Icon(Icons.add, size: 18), + onPressed: showEdit, + label: Text(localizations.add)), + const SizedBox(width: 10), + TextButton.icon( + icon: const Icon(Icons.input_rounded, size: 18), + onPressed: import, + label: Text(localizations.import), + ), + const SizedBox(width: 10), + ], + )), + const SizedBox(width: 15) + ]), + const SizedBox(height: 5), + RequestMapList(list: data.rules, windowId: widget.windowId!), + ])))); + } + + //导入js + Future import() async { + String? path; + if (Platform.isMacOS) { + path = await DesktopMultiWindow.invokeMethod(0, "pickFiles", { + "allowedExtensions": ['json'] + }); + WindowController.fromWindowId(widget.windowId!).show(); + } else { + FilePickerResult? result = + await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['json']); + path = result?.files.single.path; + } + + if (path == null) { + return; + } + try { + var json = jsonDecode(await File(path).readAsString()); + var scriptManager = (await ScriptManager.instance); + if (json is List) { + for (var item in json) { + var scriptItem = ScriptItem.fromJson(item); + await scriptManager.addScript(scriptItem, item['script']); + } + } else { + var scriptItem = ScriptItem.fromJson(json); + await scriptManager.addScript(scriptItem, json['script']); + } + + // _refreshScript(); + if (mounted) { + CustomToast.success(localizations.importSuccess).show(context); + } + setState(() {}); + } catch (e, t) { + logger.e('导入失败 $path', error: e, stackTrace: t); + if (mounted) { + CustomToast.error("${localizations.importFailed} $e").show(context); + } + } + } + + /// 添加脚本 + Future showEdit() async { + showDialog(barrierDismissible: false, context: context, builder: (_) => const RequestMapEdit()).then((value) { + if (value != null) { + setState(() {}); + } + }); + } +} + +/// 脚本列表 +class RequestMapList extends StatefulWidget { + final int windowId; + final List list; + + const RequestMapList({super.key, required this.list, required this.windowId}); + + @override + State createState() => _RequestMapListState(); +} + +class _RequestMapListState extends State { + Set selected = {}; + bool isPressed = false; + Offset? lastPressPosition; + + AppLocalizations get localizations => AppLocalizations.of(context)!; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onSecondaryTapDown: (details) => showGlobalMenu(details.globalPosition), + onTapDown: (details) { + if (selected.isEmpty) { + return; + } + if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) { + return; + } + setState(() { + selected.clear(); + }); + }, + child: Listener( + onPointerUp: (event) => isPressed = false, + onPointerDown: (event) { + lastPressPosition = event.localPosition; + if (event.buttons == kPrimaryMouseButton) { + isPressed = true; + } + }, + child: Container( + padding: const EdgeInsets.only(top: 10), + height: 530, + decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))), + child: SingleChildScrollView( + child: Column(children: [ + Row(mainAxisAlignment: MainAxisAlignment.start, children: [ + Container(width: 200, padding: const EdgeInsets.only(left: 10), child: Text(localizations.name)), + SizedBox(width: 50, child: Text(localizations.enable, textAlign: TextAlign.center)), + const VerticalDivider(), + const Expanded(child: Text("URL")), + ]), + const Divider(thickness: 0.5), + Column(children: rows(widget.list)) + ]))))); + } + + List rows(List list) { + var primaryColor = Theme.of(context).colorScheme.primary; + + 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: () => showEdit(index), + onSecondaryTapDown: (details) => showMenus(details, index), + onHover: (hover) { + if (isPressed && !selected.contains(index)) { + setState(() { + selected.add(index); + }); + } + }, + onTap: () { + if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) { + setState(() { + selected.contains(index) ? selected.remove(index) : selected.add(index); + }); + return; + } + if (selected.isEmpty) { + return; + } + setState(() { + selected.clear(); + }); + }, + child: Container( + color: selected.contains(index) + ? primaryColor.withOpacity(0.6) + : 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.6, + child: SwitchWidget( + value: list[index].enabled, + onChanged: (val) { + list[index].enabled = val; + _refreshConfig(); + }))), + const SizedBox(width: 20), + Expanded(child: Text(list[index].url, style: const TextStyle(fontSize: 13))), + ], + ))); + }); + } + + void showGlobalMenu(Offset offset) { + showContextMenu(context, offset, items: [ + PopupMenuItem(height: 35, child: Text(localizations.newBuilt), onTap: () => showEdit()), + PopupMenuItem(height: 35, child: Text(localizations.export), onTap: () => export(selected.toList())), + const PopupMenuDivider(), + PopupMenuItem(height: 35, child: Text(localizations.enableSelect), onTap: () => enableStatus(true)), + PopupMenuItem(height: 35, child: Text(localizations.disableSelect), onTap: () => enableStatus(false)), + const PopupMenuDivider(), + PopupMenuItem(height: 35, child: Text(localizations.deleteSelect), onTap: () => remove(selected.toList())), + ]); + } + + //点击菜单 + void showMenus(TapDownDetails details, int index) { + if (selected.length > 1) { + showGlobalMenu(details.globalPosition); + return; + } + setState(() { + selected.add(index); + }); + + showContextMenu(context, details.globalPosition, items: [ + PopupMenuItem(height: 35, child: Text(localizations.edit), onTap: () => showEdit(index)), + PopupMenuItem(height: 35, child: Text(localizations.export), onTap: () => export([index])), + PopupMenuItem( + height: 35, + child: widget.list[index].enabled ? Text(localizations.disabled) : Text(localizations.enable), + onTap: () { + widget.list[index].enabled = !widget.list[index].enabled; + _refreshConfig(); + }), + const PopupMenuDivider(), + PopupMenuItem( + height: 35, + child: Text(localizations.delete), + onTap: () async { + var scriptManager = await ScriptManager.instance; + await scriptManager.removeScript(index); + _refreshConfig(); + }), + ]).then((value) { + if (mounted) { + setState(() { + selected.remove(index); + }); + } + }); + } + + Future showEdit([int? index]) async { + final item = index == null ? null : await (await RequestMapManager.instance).getMapItem(widget.list[index]); + if (!mounted) { + return; + } + + showDialog( + barrierDismissible: false, + context: context, + builder: (_) => RequestMapEdit(rule: index == null ? null : widget.list[index], item: item)).then((value) { + if (value != null) { + setState(() {}); + } + }); + } + + //导出 + Future export(List indexes) async { + if (indexes.isEmpty) return; + //文件名称 + String fileName = 'request_map.json'; + String? path; + if (Platform.isMacOS) { + path = await DesktopMultiWindow.invokeMethod(0, "saveFile", {"fileName": fileName}); + WindowController.fromWindowId(widget.windowId).show(); + } else { + path = await FilePicker.platform.saveFile(fileName: fileName); + } + if (path == null) { + return; + } + + var manager = await RequestMapManager.instance; + List json = []; + for (var idx in indexes) { + var item = widget.list[idx]; + var map = item.toJson(); + map.remove("itemPath"); + map['item'] = await manager.getMapItem(item); + json.add(map); + } + + await File(path).writeAsBytes(utf8.encode(jsonEncode(json))); + + if (mounted) FlutterToastr.show(localizations.exportSuccess, context); + } + + void enableStatus(bool enable) { + for (var idx in selected) { + widget.list[idx].enabled = enable; + } + setState(() {}); + _refreshConfig(); + } + + Future remove(List indexes) async { + if (indexes.isEmpty) return; + showConfirmDialog(context, content: localizations.confirmContent, onConfirm: () async { + var manager = await RequestMapManager.instance; + for (var idx in indexes) { + await manager.deleteRule(idx); + } + + setState(() { + selected.clear(); + }); + _refreshConfig(force: true); + + if (mounted) FlutterToastr.show(localizations.deleteSuccess, context); + }); + } +} + +///请求重写规则添加对话框 +class RequestMapEdit extends StatefulWidget { + final RequestMapRule? rule; + final RequestMapItem? item; + final int? windowId; + + const RequestMapEdit({super.key, this.rule, this.windowId, this.item}); + + @override + State createState() { + return _RequestMapEditState(); + } +} + +class _RequestMapEditState extends State { + final mapLocalKey = GlobalKey(); + final mapScriptKey = GlobalKey(); + + late RequestMapRule rule; + + late RequestMapType mapType; + late TextEditingController nameInput; + late TextEditingController urlInput; + + AppLocalizations get localizations => AppLocalizations.of(context)!; + + @override + void initState() { + super.initState(); + rule = widget.rule ?? RequestMapRule(url: '', type: RequestMapType.local); + mapType = rule.type; + nameInput = TextEditingController(text: rule.name); + urlInput = TextEditingController(text: rule.url); + } + + @override + void dispose() { + urlInput.dispose(); + nameInput.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + GlobalKey formKey = GlobalKey(); + bool isEN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'en'); + + return AlertDialog( + scrollable: true, + titlePadding: const EdgeInsets.only(top: 10, left: 20), + actionsPadding: const EdgeInsets.only(right: 15, bottom: 15), + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5), + title: Row(children: [ + Text(localizations.requestRewriteRule, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), + ]), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)), + content: Container( + width: 550, + constraints: const BoxConstraints(minHeight: 200, maxHeight: 530), + child: Form( + key: formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + SizedBox(width: 55, child: Text('${localizations.enable}:')), + SwitchWidget(value: rule.enabled, onChanged: (val) => rule.enabled = val, scale: 0.8) + ]), + const SizedBox(height: 5), + textField('${localizations.name}:', nameInput, localizations.pleaseEnter), + const SizedBox(height: 5), + textField('URL:', urlInput, 'https://www.example.com/api/*', required: true), + const SizedBox(height: 5), + Row(children: [ + SizedBox(width: 60, child: Text('${localizations.action}:')), + SizedBox( + width: 150, + height: 33, + child: DropdownButtonFormField( + onSaved: (val) => rule.type = val!, + value: mapType, + decoration: InputDecoration( + errorStyle: const TextStyle(height: 0, fontSize: 0), + contentPadding: const EdgeInsets.only(left: 7, right: 7), + focusedBorder: focusedBorder(), + border: const OutlineInputBorder()), + items: RequestMapType.values + .map((e) => DropdownMenuItem( + value: e, + child: Text(isEN ? e.name : e.label, style: const TextStyle(fontSize: 13)))) + .toList(), + onChanged: onChangeType, + )), + const SizedBox(width: 10), + ]), + const SizedBox(height: 10), + mapRule(), + ]))), + actions: [ + ElevatedButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop()), + FilledButton( + child: Text(localizations.save), + onPressed: () async { + if (!(formKey.currentState as FormState).validate()) { + FlutterToastr.show(localizations.cannotBeEmpty, context, position: FlutterToastr.center); + return; + } + + (formKey.currentState as FormState).save(); + rule.name = nameInput.text; + rule.url = urlInput.text; + rule.type = mapType; + RequestMapItem item; + if (mapType == RequestMapType.local) { + item = mapLocalKey.currentState!.getRequestMapItem(); + } else { + String? scriptCode = mapScriptKey.currentState?.getScriptCode(); + item = widget.item ?? RequestMapItem(); + item.script = scriptCode; + } + + var requestMapManager = await RequestMapManager.instance; + var index = requestMapManager.rules.indexOf(rule); + if (index >= 0) { + requestMapManager.updateRule(rule, item); + } else { + await requestMapManager.addRule(rule, item); + } + + DesktopMultiWindow.invokeMethod(0, "refreshRequestMap"); + if (mounted) { + Navigator.of(this.context).pop(rule); + } + }) + ]); + } + + void onChangeType(RequestMapType? val) async { + if (mapType == val) return; + mapType = val!; + setState(() { + // rewriteReplaceKey.currentState?.initItems(ruleType, items); + // rewriteUpdateKey.currentState?.initItems(ruleType, items); + }); + } + + Widget mapRule() { + if (mapType == RequestMapType.script) { + return DesktopMapScript(key: mapScriptKey, script: widget.item?.script); + } + + return DesktopMapLocal(key: mapLocalKey, item: widget.item, windowId: widget.windowId); + } + + Widget textField(String label, TextEditingController controller, String hint, + {bool required = false, FormFieldSetter? onSaved}) { + return Row(children: [ + SizedBox(width: 60, child: Text(label)), + Expanded( + child: TextFormField( + controller: controller, + style: const TextStyle(fontSize: 14), + validator: (val) => val?.isNotEmpty == true || !required ? null : "", + onSaved: onSaved, + decoration: InputDecoration( + hintText: hint, + constraints: const BoxConstraints(minHeight: 38), + hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14), + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), + errorStyle: const TextStyle(height: 0, fontSize: 0), + focusedBorder: focusedBorder(), + isDense: true, + border: const OutlineInputBorder()), + )) + ]); + } + + InputBorder focusedBorder() { + return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2)); + } +} diff --git a/lib/ui/desktop/toolbar/setting/request_map/map_local.dart b/lib/ui/desktop/toolbar/setting/request_map/map_local.dart new file mode 100644 index 0000000..8ed9335 --- /dev/null +++ b/lib/ui/desktop/toolbar/setting/request_map/map_local.dart @@ -0,0 +1,402 @@ +/* + * Copyright 2023 Hongen Wang All rights reserved. + * + * 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'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:proxypin/l10n/app_localizations.dart'; +import 'package:proxypin/network/components/manager/request_map_manager.dart'; +import 'package:proxypin/network/components/manager/rewrite_rule.dart'; +import 'package:proxypin/ui/component/state_component.dart'; + +/// 重写替换 +/// @author wanghongen +/// 2023/10/8 +class DesktopMapLocal extends StatefulWidget { + final int? windowId; + final RequestMapItem? item; + + const DesktopMapLocal({super.key, this.item, this.windowId}); + + @override + State createState() => MapLocaleState(); +} + +class MapLocaleState extends State { + final _headerKey = GlobalKey(); + final bodyTextController = TextEditingController(); + + RxString bodyType = RxString(ReplaceBodyType.text.name); + Rxn bodyFile = Rxn(); + TextEditingController statusCodeController = TextEditingController(text: '200'); + + AppLocalizations get localizations => AppLocalizations.of(context)!; + + @override + initState() { + super.initState(); + initItem(widget.item); + } + + @override + dispose() { + bodyTextController.dispose(); + statusCodeController.dispose(); + super.dispose(); + } + + ///初始化重写项 + void initItem(RequestMapItem? item) { + if (item == null) { + return; + } + statusCodeController.text = item.statusCode?.toString() ?? '200'; + bodyTextController.text = item.body ?? ''; + bodyType.value = item.bodyType ?? ReplaceBodyType.text.name; + } + + RequestMapItem getRequestMapItem() { + RequestMapItem item = widget.item ?? RequestMapItem(); + var headers = _headerKey.currentState?.getHeaders(); + item.statusCode = int.tryParse(statusCodeController.text) ?? 200; + item.headers = headers; + item.body = bodyTextController.text; + item.bodyType = item.bodyType ?? ReplaceBodyType.text.name; + if (item.bodyType == ReplaceBodyType.file.name) { + item.bodyFile = bodyFile.value; + } else { + item.bodyFile = null; + } + return item; + } + + @override + Widget build(BuildContext context) { + List tabs = [localizations.statusCode, localizations.responseHeader, localizations.responseBody]; + + return Container( + constraints: const BoxConstraints(maxHeight: 340), + child: DefaultTabController( + length: tabs.length, + initialIndex: tabs.length - 1, + child: Scaffold( + appBar: tabBar(tabs), + body: TabBarView(children: [ + KeepAliveWrapper(child: statusCodeEdit()), + KeepAliveWrapper(child: headers()), + KeepAliveWrapper(child: body()) + ]), + )), + ); + } + + //tabBar + TabBar tabBar(List tabs) { + return TabBar( + tabs: tabs + .map((label) => Tab( + height: 38, + child: Text(label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + )) + .toList()); + } + + //body + Widget body() { + bool isEN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'en'); + + return Obx(() => Column(children: [ + Row(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ + const SizedBox(width: 5), + Text("${localizations.type}: "), + SizedBox( + width: 90, + child: DropdownButtonFormField( + value: bodyType.value, + focusColor: Colors.transparent, + itemHeight: 48, + decoration: const InputDecoration( + contentPadding: EdgeInsets.all(10), isDense: true, border: InputBorder.none), + items: ReplaceBodyType.values + .map((e) => DropdownMenuItem( + value: e.name, + child: Text(isEN ? e.name.toUpperCase() : e.label, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)))) + .toList(), + onChanged: (val) => bodyType.value = val ?? ReplaceBodyType.text.name)), + ]), + const SizedBox(height: 10), + if (bodyType.value == ReplaceBodyType.file.name) + fileBodyEdit() + else + TextFormField( + controller: bodyTextController, + style: const TextStyle(fontSize: 14), + maxLines: 11, + decoration: decoration(localizations.replaceBodyWith, + hintText: '${localizations.example} {"code":"200","data":{}}')), + ])); + } + + Widget fileBodyEdit() { + //选择文件 删除 + return Obx(() => Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Expanded( + child: bodyFile.value == null + ? Container(height: 50) + : Container( + padding: const EdgeInsets.all(5), + foregroundDecoration: + BoxDecoration(border: Border.all(color: Theme.of(context).colorScheme.primary, width: 1)), + child: Text(bodyFile.value ?? ''))), + const SizedBox(width: 10), + FilledButton( + onPressed: () async { + String? path; + if (Platform.isMacOS) { + path = await DesktopMultiWindow.invokeMethod(0, "pickFiles"); + if (widget.windowId != null) WindowController.fromWindowId(widget.windowId!).show(); + } else { + FilePickerResult? result = await FilePicker.platform.pickFiles(); + path = result?.files.single.path; + } + + if (path == null) { + return; + } + bodyFile.value = path; + }, + child: Text(localizations.selectFile, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500))), + const SizedBox(width: 10), + FilledButton( + onPressed: () { + setState(() { + bodyFile.value = null; + }); + }, + child: Text(localizations.delete, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500))), + ])); + } + + //headers + Widget headers() { + return Headers(headers: widget.item?.headers, key: _headerKey); + } + + Widget textField(String label, dynamic value, String hint, {ValueChanged? onChanged}) { + return Row(children: [ + SizedBox(width: 80, child: Text(label)), + Expanded( + child: TextFormField( + initialValue: value, + onChanged: onChanged, + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14), + contentPadding: const EdgeInsets.all(10), + errorStyle: const TextStyle(height: 0, fontSize: 0), + focusedBorder: focusedBorder(), + isDense: true, + border: const OutlineInputBorder()), + )) + ]); + } + + Widget statusCodeEdit() { + return Container( + padding: const EdgeInsets.all(10), + child: Column(children: [ + Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ + Text(localizations.statusCode), + const SizedBox(width: 10), + SizedBox( + width: 100, + child: TextFormField( + controller: statusCodeController, + style: const TextStyle(fontSize: 14), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: InputDecoration( + contentPadding: const EdgeInsets.all(10), + focusedBorder: focusedBorder(), + isDense: true, + border: const OutlineInputBorder()), + )), + const SizedBox(width: 10), + ]) + ])); + } + + InputDecoration decoration(String label, {String? hintText}) { + Color color = Theme.of(context).colorScheme.primary; + // Color color = Colors.blueAccent; + return InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.always, + labelText: label, + hintStyle: TextStyle(color: Colors.grey.shade500), + hintText: hintText, + isDense: true, + border: OutlineInputBorder(borderSide: BorderSide(width: 0.8, color: color)), + enabledBorder: OutlineInputBorder(borderSide: BorderSide(width: 1.5, color: color)), + focusedBorder: OutlineInputBorder(borderSide: BorderSide(width: 2, color: color))); + } + + InputBorder focusedBorder() { + return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2)); + } +} + +///请求头 +class Headers extends StatefulWidget { + final Map? headers; + + const Headers({super.key, this.headers}); + + @override + State createState() { + return HeadersState(); + } +} + +class HeadersState extends State with AutomaticKeepAliveClientMixin { + final Map _headers = {}; + + @override + bool get wantKeepAlive => true; + + AppLocalizations get localizations => AppLocalizations.of(context)!; + + @override + void initState() { + super.initState(); + if (widget.headers == null) { + _headers[TextEditingController()] = TextEditingController(); + return; + } + + setHeaders(widget.headers); + } + + void setHeaders(Map? headers) { + _clear(); + headers?.forEach((name, value) { + _headers[TextEditingController(text: name)] = TextEditingController(text: value); + }); + _headers[TextEditingController()] = TextEditingController(); + } + + ///获取所有请求头 + Map getHeaders() { + var headers = {}; + _headers.forEach((name, value) { + if (name.text.isEmpty) { + return; + } + headers[name.text] = value.text; + }); + return headers; + } + + @override + dispose() { + _clear(); + super.dispose(); + } + + void _clear() { + _headers.forEach((key, value) { + key.dispose(); + value.dispose(); + }); + _headers.clear(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + + var list = _buildRows(); + + return Column(children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 10, bottom: 10), + child: ListView.separated( + shrinkWrap: true, + separatorBuilder: (context, index) => + index == list.length ? const SizedBox() : const Divider(thickness: 0.2), + itemBuilder: (context, index) => list[index], + itemCount: list.length))), + TextButton( + child: Text("${localizations.add}Header", textAlign: TextAlign.center), + onPressed: () { + setState(() { + _headers[TextEditingController()] = TextEditingController(); + }); + }, + ), + ]); + } + + List _buildRows() { + List list = []; + + _headers.forEach((key, val) { + list.add(_row( + _cell(key, isKey: true), + _cell(val), + Padding( + padding: const EdgeInsets.only(right: 15), + child: InkWell( + onTap: () { + setState(() { + _headers.remove(key); + }); + }, + child: const Icon(Icons.remove_circle_outline, size: 16))))); + }); + + return list; + } + + Widget _cell(TextEditingController val, {bool isKey = false}) { + return Container( + padding: const EdgeInsets.only(right: 5), + child: TextFormField( + style: TextStyle(fontSize: 12, fontWeight: isKey ? FontWeight.w500 : null), + controller: val, + minLines: 1, + maxLines: 3, + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + enabledBorder: OutlineInputBorder(borderSide: BorderSide(width: 0.5, color: Colors.grey)), + hintStyle: TextStyle(fontSize: 12, color: Colors.grey), + hintText: isKey ? "Key" : "Value"))); + } + + Widget _row(Widget key, Widget val, Widget? op) { + return Row(children: [ + Expanded(flex: 4, child: key), + const Text(": ", style: TextStyle(color: Colors.deepOrangeAccent)), + Expanded(flex: 6, child: val), + op ?? const SizedBox() + ]); + } +} diff --git a/lib/ui/desktop/toolbar/setting/request_map/map_scipt.dart b/lib/ui/desktop/toolbar/setting/request_map/map_scipt.dart new file mode 100644 index 0000000..152f8f7 --- /dev/null +++ b/lib/ui/desktop/toolbar/setting/request_map/map_scipt.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_code_editor/flutter_code_editor.dart'; +import 'package:flutter_highlight/themes/monokai-sublime.dart'; +import 'package:highlight/languages/javascript.dart'; +import 'package:proxypin/l10n/app_localizations.dart'; +import 'package:proxypin/network/components/manager/script_manager.dart'; + +class DesktopMapScript extends StatefulWidget { + final String? script; + + const DesktopMapScript({super.key, this.script}); + + @override + State createState() => MapScriptState(); +} + +class MapScriptState extends State { + static String template = """ +async function onRequest(context, request) { + console.log(request.url); + //use fetch API request + // var result = await fetch('https://www.baidu.com/'); + var response = { + statusCode: 200, + body: 'Hello, world!', + headers: { + 'Content-Type': 'text/plain', + 'X-My-Header': 'My-Value' + } + }; + return response; +} + """; + late CodeController script; + + AppLocalizations get localizations => AppLocalizations.of(context)!; + + String getScriptCode() { + return script.text; + } + + @override + void initState() { + super.initState(); + script = CodeController(language: javascript, text: widget.script ?? template); + } + + @override + void dispose() { + script.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 320, + child: CodeTheme( + data: CodeThemeData(styles: monokaiSublimeTheme), + child: + SingleChildScrollView(child: CodeField(textStyle: const TextStyle(fontSize: 13), controller: script)))); + } +} diff --git a/lib/ui/desktop/toolbar/setting/request_rewrite.dart b/lib/ui/desktop/toolbar/setting/request_rewrite.dart index a7702e9..9155544 100644 --- a/lib/ui/desktop/toolbar/setting/request_rewrite.dart +++ b/lib/ui/desktop/toolbar/setting/request_rewrite.dart @@ -152,7 +152,7 @@ class RequestRewriteState extends State { } //导入js - import() async { + Future import() async { String? path; if (Platform.isMacOS) { path = await DesktopMultiWindow.invokeMethod(0, "pickFiles", { diff --git a/lib/ui/desktop/toolbar/setting/rewrite/rewrite_replace.dart b/lib/ui/desktop/toolbar/setting/rewrite/rewrite_replace.dart index 3ca8961..3bef0d3 100644 --- a/lib/ui/desktop/toolbar/setting/rewrite/rewrite_replace.dart +++ b/lib/ui/desktop/toolbar/setting/rewrite/rewrite_replace.dart @@ -539,7 +539,7 @@ class HeadersState extends State with AutomaticKeepAliveClientMixin { _headers.remove(key); }); }, - child: const Icon(Icons.remove_circle, size: 16))))); + child: const Icon(Icons.remove_circle_outline, size: 16))))); }); return list; diff --git a/lib/ui/desktop/toolbar/setting/script.dart b/lib/ui/desktop/toolbar/setting/script.dart index 578bf50..e027cab 100644 --- a/lib/ui/desktop/toolbar/setting/script.dart +++ b/lib/ui/desktop/toolbar/setting/script.dart @@ -150,7 +150,7 @@ class _ScriptWidgetState extends State { ])))); } - consoleLog() { + void consoleLog() { openScriptConsoleWindow(); } @@ -668,7 +668,7 @@ class _ScriptListState extends State { _refreshScript(); } - removeScripts(List indexes) async { + Future removeScripts(List indexes) async { if (indexes.isEmpty) return; showConfirmDialog(context, content: localizations.confirmContent, onConfirm: () async { var scriptManager = await ScriptManager.instance; diff --git a/lib/ui/desktop/toolbar/setting/setting.dart b/lib/ui/desktop/toolbar/setting/setting.dart index 093939d..3575e42 100644 --- a/lib/ui/desktop/toolbar/setting/setting.dart +++ b/lib/ui/desktop/toolbar/setting/setting.dart @@ -28,6 +28,7 @@ import 'package:proxypin/ui/desktop/toolbar/setting/about.dart'; import 'package:proxypin/ui/desktop/toolbar/setting/external_proxy.dart'; import 'package:proxypin/ui/desktop/toolbar/setting/hosts.dart'; import 'package:proxypin/ui/desktop/toolbar/setting/request_block.dart'; +import 'package:proxypin/ui/desktop/toolbar/setting/request_map.dart'; import 'filter.dart'; @@ -75,6 +76,7 @@ class _SettingState extends State { item(localizations.hosts, onPressed: hosts), item(localizations.requestBlock, onPressed: showRequestBlock), item(localizations.requestRewrite, onPressed: requestRewrite), + item("请求映射", onPressed: requestMapLocal), item(localizations.script, onPressed: () => MultiWindow.openWindow(localizations.script, 'ScriptWidget', size: const Size(800, 700))), item(localizations.externalProxy, onPressed: setExternalProxy), @@ -92,12 +94,12 @@ class _SettingState extends State { child: Text(text, style: const TextStyle(fontSize: 14)))); } - showAbout() { + void showAbout() { showDialog(context: context, builder: (context) => DesktopAbout()); } ///设置外部代理地址 - setExternalProxy() { + void setExternalProxy() { showDialog( barrierDismissible: false, context: context, @@ -112,6 +114,18 @@ class _SettingState extends State { MultiWindow.openWindow(localizations.requestRewrite, 'RequestRewriteWidget', size: const Size(800, 750)); } + ///请求本地映射 + void requestMapLocal() async { + if (!mounted) return; + // MultiWindow.openWindow(localizations.requestRewrite, 'RequestRewriteWidget', size: const Size(800, 750)); + showDialog( + barrierDismissible: false, + context: context, + builder: (context) { + return RequestMapPage(); + }); + } + ///show域名过滤Dialog void hostFilter() { showDialog( diff --git a/lib/ui/mobile/setting/script.dart b/lib/ui/mobile/setting/script.dart index e280ea3..6acd092 100644 --- a/lib/ui/mobile/setting/script.dart +++ b/lib/ui/mobile/setting/script.dart @@ -46,8 +46,8 @@ class MobileScript extends StatefulWidget { bool _refresh = false; /// 刷新脚本 -void _refreshScript() { - if (_refresh) { +void _refreshScript({bool force = false}) { + if (_refresh && !force) { return; } _refresh = true; @@ -447,7 +447,7 @@ class _ScriptEditState extends State { await scriptManager.updateScript(widget.scriptItem!, script.text); } - _refreshScript(); + _refreshScript(force: true); if (context.mounted) { FlutterToastr.show(localizations.saveSuccess, context); Navigator.of(context).maybePop(true); @@ -671,7 +671,7 @@ class _ScriptListState extends State { text: localizations.delete, onPressed: () async { await (await ScriptManager.instance).removeScript(index); - _refreshScript(); + _refreshScript(force: true); if (context.mounted) FlutterToastr.show(localizations.importSuccess, context); }), Container(color: Theme.of(context).hoverColor, height: 8), @@ -755,7 +755,7 @@ class _ScriptListState extends State { setState(() { selected.clear(); }); - _refreshScript(); + _refreshScript(force: true); if (mounted) FlutterToastr.show(localizations.deleteSuccess, context); }); diff --git a/pubspec.yaml b/pubspec.yaml index 8dfcc0f..130caa5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,9 +15,9 @@ dependencies: intl: any cupertino_icons: ^1.0.8 pointycastle: ^4.0.0 - logger: ^2.5.0 + logger: ^2.6.1 date_format: ^2.0.9 - window_manager: ^0.5.0 + window_manager: ^0.5.1 windows_single_instance: ^1.0.1 desktop_multi_window: git: @@ -26,7 +26,7 @@ dependencies: path_provider: ^2.1.5 file_picker: ^10.2.0 proxy_manager: ^0.0.3 - permission_handler: ^12.0.0+1 + permission_handler: ^12.0.1 flutter_toastr: ^1.0.3 share_plus: ^11.0.0 flutter_js: @@ -41,8 +41,9 @@ dependencies: device_info_plus: ^11.5.0 shared_preferences: ^2.5.3 image_pickers: ^2.0.6 - url_launcher: ^6.3.1 + url_launcher: ^6.3.2 toastification: ^3.0.2 + get: ^4.7.2 qr_flutter: ^4.1.0 flutter_qr_reader_plus: ^1.0.6