diff --git a/lib/network/channel/network.dart b/lib/network/channel/network.dart index 11e3158..882224b 100644 --- a/lib/network/channel/network.dart +++ b/lib/network/channel/network.dart @@ -268,7 +268,7 @@ class Client extends Network { // host = host.substring(1, host.length - 1); // } - logger.d('Connecting to $host:${hostAndPort.port}'); + // logger.d('Connecting to $host:${hostAndPort.port}'); return Socket.connect(host, hostAndPort.port, timeout: timeout).then((socket) { if (socket.address.type != InternetAddressType.unix) { socket.setOption(SocketOption.tcpNoDelay, true); diff --git a/lib/network/components/manager/script_manager.dart b/lib/network/components/manager/script_manager.dart index 38438a0..bbece23 100644 --- a/lib/network/components/manager/script_manager.dart +++ b/lib/network/components/manager/script_manager.dart @@ -36,8 +36,6 @@ class ScriptManager { // e.g. Add/Update/Remove:Queries、Headers、Body async function onRequest(context, request) { console.log(request.url); - //URL queries - //request.queries["name"] = "value"; //Update or add Header //request.headers["X-New-Headers"] = "My-Value"; @@ -312,28 +310,48 @@ class LogInfo { class ScriptItem { bool enabled = true; String? name; - String url; + List urls; String? scriptPath; - RegExp? urlReg; + List? urlRegs; - ScriptItem(this.enabled, this.name, this.url, {this.scriptPath}); + ScriptItem(this.enabled, this.name, dynamic urls, {this.scriptPath}) + : urls = urls is String + ? (urls.contains(',') ? urls.split(',').map((e) => e.trim()).toList() : [urls]) + : (urls is List ? urls : []); - //匹配url + // 匹配url,任意一个规则匹配即可 bool match(String url) { - urlReg ??= RegExp(this.url.replaceAll("*", ".*")); - return urlReg!.hasMatch(url); + urlRegs ??= urls.map((u) => RegExp(u.replaceAll("*", ".*"))).toList(); + for (final reg in urlRegs!) { + if (reg!.hasMatch(url)) return true; + } + return false; } factory ScriptItem.fromJson(Map json) { - return ScriptItem(json['enabled'], json['name'], json['url'], scriptPath: json['scriptPath']); + final urlField = json['url']; + List urls; + if (urlField is List) { + urls = urlField.cast(); + } else if (urlField is String) { + urls = urlField.contains(',') ? urlField.split(',').map((e) => e.trim()).toList() : [urlField]; + } else { + urls = []; + } + return ScriptItem(json['enabled'], json['name'], urls, scriptPath: json['scriptPath']); } Map toJson() { - return {'enabled': enabled, 'name': name, 'url': url, 'scriptPath': scriptPath}; + return { + 'enabled': enabled, + 'name': name, + 'url': urls.length == 1 ? urls[0] : urls, + 'scriptPath': scriptPath + }; } @override String toString() { - return 'ScriptItem{enabled: $enabled, name: $name, url: $url, scriptPath: $scriptPath}'; + return 'ScriptItem{enabled: $enabled, name: $name, url: $urls, scriptPath: $scriptPath}'; } } diff --git a/lib/network/handle/http_proxy_handle.dart b/lib/network/handle/http_proxy_handle.dart index 2003ff3..0164916 100644 --- a/lib/network/handle/http_proxy_handle.dart +++ b/lib/network/handle/http_proxy_handle.dart @@ -87,8 +87,8 @@ class HttpProxyChannelHandler extends ChannelHandler { //实现抓包代理转发 if (httpRequest.method != HttpMethod.connect) { - log.d( - "[${channel.id}] streamId:${httpRequest.streamId ?? ''} ${httpRequest.protocolVersion} ${httpRequest.method.name} ${httpRequest.requestUrl}"); + // log.d( + // "[${channel.id}] streamId:${httpRequest.streamId ?? ''} ${httpRequest.protocolVersion} ${httpRequest.method.name} ${httpRequest.requestUrl}"); if (HostFilter.filter(httpRequest.hostAndPort?.host)) { await remoteChannel.write(channelContext, httpRequest); return; diff --git a/lib/network/util/proxy_helper.dart b/lib/network/util/proxy_helper.dart index 5766ff7..613f644 100644 --- a/lib/network/util/proxy_helper.dart +++ b/lib/network/util/proxy_helper.dart @@ -44,10 +44,11 @@ class ProxyHelper { 'blacklist': HostFilter.blacklist.toJson(), 'scripts': await ScriptManager.instance.then((script) { var list = script.list.map((e) async { - return {'name': e.name, 'enabled': e.enabled, 'url': e.url, 'script': await script.getScript(e)}; + return {'name': e.name, 'enabled': e.enabled, 'url': e.urls, 'script': await script.getScript(e)}; }); return Future.wait(list); }), + }; response.body = utf8.encode(json.encode(body)); channel.writeAndClose(channelContext, response); diff --git a/lib/ui/desktop/request/request.dart b/lib/ui/desktop/request/request.dart index fc692dd..138edee 100644 --- a/lib/ui/desktop/request/request.dart +++ b/lib/ui/desktop/request/request.dart @@ -215,7 +215,7 @@ class _RequestWidgetState extends State { onClick: (_) async { var scriptManager = await ScriptManager.instance; var url = widget.request.domainPath; - var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.url == url); + var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.urls.contains(url)); String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem); if (!mounted) return; diff --git a/lib/ui/desktop/setting/script.dart b/lib/ui/desktop/setting/script.dart index 426e2cd..ba6b681 100644 --- a/lib/ui/desktop/setting/script.dart +++ b/lib/ui/desktop/setting/script.dart @@ -335,7 +335,7 @@ class ScriptEdit extends StatefulWidget { class _ScriptEditState extends State { late CodeController script; late TextEditingController nameController; - late TextEditingController urlController; + late List urlControllers; AppLocalizations get localizations => AppLocalizations.of(context)!; @@ -344,14 +344,18 @@ class _ScriptEditState extends State { super.initState(); script = CodeController(language: javascript, text: widget.script ?? ScriptManager.template); nameController = TextEditingController(text: widget.scriptItem?.name ?? widget.title); - urlController = TextEditingController(text: widget.scriptItem?.url ?? widget.url); + final urls = widget.scriptItem?.urls ?? (widget.url != null && widget.url!.isNotEmpty ? [widget.url!] : []); + urlControllers = + urls.isNotEmpty ? urls.map((u) => TextEditingController(text: u)).toList() : [TextEditingController()]; } @override void dispose() { script.dispose(); nameController.dispose(); - urlController.dispose(); + for (final c in urlControllers) { + c.dispose(); + } super.dispose(); } @@ -361,72 +365,130 @@ class _ScriptEditState extends State { bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh'); return AlertDialog( - scrollable: true, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)), - titlePadding: const EdgeInsets.only(left: 15, top: 5, right: 15), - title: Row(children: [ - Text(localizations.scriptEdit, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), - const SizedBox(width: 10), - Text.rich(TextSpan( - text: localizations.useGuide, - style: const TextStyle(color: Colors.blue, fontSize: 14), - recognizer: TapGestureRecognizer() - ..onTap = () => DesktopMultiWindow.invokeMethod( - 0, - "launchUrl", - isCN - ? 'https://gitee.com/wanghongenpin/proxypin/wikis/%E8%84%9A%E6%9C%AC' - : 'https://github.com/wanghongenpin/proxypin/wiki/Script'))), - 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: Text(localizations.cancel)), - FilledButton( - onPressed: () async { - if (!(formKey.currentState as FormState).validate()) { - FlutterToastr.show("${localizations.name} URL ${localizations.cannotBeEmpty}", context, - position: FlutterToastr.top); - return; - } - //新增 - if (widget.scriptItem == null) { - var scriptItem = ScriptItem(true, nameController.text, urlController.text); - await (await ScriptManager.instance).addScript(scriptItem, script.text); - } else { - widget.scriptItem?.name = nameController.text; - widget.scriptItem?.url = urlController.text; - widget.scriptItem?.urlReg = null; - (await ScriptManager.instance).updateScript(widget.scriptItem!, script.text); - } - - _refreshScript(); - if (context.mounted) { - Navigator.of(context).maybePop(true); - } - }, - child: Text(localizations.save)), - ], - content: Form( - key: formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - textField("${localizations.name}:", nameController, localizations.pleaseEnter), - const SizedBox(height: 10), - textField("URL:", urlController, "github.com/api/*", keyboardType: TextInputType.url), - const SizedBox(height: 10), - Text("${localizations.script}:"), - const SizedBox(height: 5), - SizedBox( - width: 850, - height: 380, - child: CodeTheme( - data: CodeThemeData(styles: monokaiSublimeTheme), - child: SingleChildScrollView( - child: CodeField(textStyle: const TextStyle(fontSize: 13), controller: script)))) - ], - ))); + scrollable: true, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)), + titlePadding: const EdgeInsets.only(left: 15, top: 5, right: 15), + title: Row(children: [ + Text(localizations.scriptEdit, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), + const SizedBox(width: 10), + Text.rich(TextSpan( + text: localizations.useGuide, + style: const TextStyle(color: Colors.blue, fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () => DesktopMultiWindow.invokeMethod( + 0, + "launchUrl", + isCN + ? 'https://gitee.com/wanghongenpin/proxypin/wikis/%E8%84%9A%E6%9C%AC' + : 'https://github.com/wanghongenpin/proxypin/wiki/Script'))), + 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: Text(localizations.cancel)), + FilledButton( + onPressed: () async { + if (!(formKey.currentState as FormState).validate()) { + FlutterToastr.show("${localizations.name} URL ${localizations.cannotBeEmpty}", context, + position: FlutterToastr.top); + return; + } + final urls = urlControllers.map((c) => c.text.trim()).where((u) => u.isNotEmpty).toSet().toList(); + if (urls.isEmpty) { + FlutterToastr.show("URL ${localizations.cannotBeEmpty}", context, position: FlutterToastr.top); + return; + } + if (widget.scriptItem == null) { + var scriptItem = ScriptItem(true, nameController.text, urls); + await (await ScriptManager.instance).addScript(scriptItem, script.text); + } else { + widget.scriptItem?.name = nameController.text; + widget.scriptItem?.urls = urls; + widget.scriptItem?.urlRegs = null; + (await ScriptManager.instance).updateScript(widget.scriptItem!, script.text); + } + _refreshScript(); + if (context.mounted) { + Navigator.of(context).maybePop(true); + } + }, + child: Text(localizations.save)), + ], + content: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + textField("${localizations.name}:", nameController, localizations.pleaseEnter), + const SizedBox(height: 3), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text("URL(s):"), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.add_circle_outline, size: 20), + tooltip: localizations.add, + onPressed: () { + setState(() { + urlControllers.add(TextEditingController()); + }); + }, + ), + ], + ), + ...List.generate( + urlControllers.length, + (i) => Row( + children: [ + Expanded( + child: TextFormField( + controller: urlControllers[i], + validator: (val) => val?.isNotEmpty == true ? null : "", + keyboardType: TextInputType.url, + decoration: InputDecoration( + hintText: "github.com/api/*", + hintStyle: const TextStyle(fontSize: 14, color: Colors.grey), + contentPadding: const EdgeInsets.all(10), + errorStyle: const TextStyle(height: 0, fontSize: 0), + focusedBorder: focusedBorder(), + isDense: true, + border: const OutlineInputBorder(), + ), + ), + ), + if (urlControllers.length > 1) + IconButton( + icon: const Icon(Icons.remove_circle_outline, color: Colors.red), + tooltip: localizations.delete, + onPressed: () { + setState(() { + urlControllers[i].dispose(); + urlControllers.removeAt(i); + }); + }, + ), + ], + )), + ], + ), + const SizedBox(height: 10), + Text( + "${localizations.script}:", + ), + const SizedBox(height: 5), + SizedBox( + width: 850, + height: 380, + 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}) { @@ -439,6 +501,7 @@ class _ScriptEditState extends State { keyboardType: keyboardType, decoration: InputDecoration( hintText: hint, + hintStyle: const TextStyle(fontSize: 14, color: Colors.grey), contentPadding: const EdgeInsets.all(10), errorStyle: const TextStyle(height: 0, fontSize: 0), focusedBorder: focusedBorder(), @@ -573,13 +636,13 @@ class _ScriptListState extends State { _refreshScript(); }))), const SizedBox(width: 20), - Expanded(child: Text(list[index].url, style: const TextStyle(fontSize: 13))), + Expanded(child: Text(list[index].urls.join(', '), style: const TextStyle(fontSize: 13))), ], ))); }); } - showGlobalMenu(Offset offset) { + 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())), diff --git a/lib/ui/mobile/request/favorite.dart b/lib/ui/mobile/request/favorite.dart index 8a8e079..648ab02 100644 --- a/lib/ui/mobile/request/favorite.dart +++ b/lib/ui/mobile/request/favorite.dart @@ -260,12 +260,12 @@ class _FavoriteItemState extends State<_FavoriteItem> { Navigator.maybePop(context); var scriptManager = await ScriptManager.instance; var url = request.domainPath; - var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.url == url); + var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.urls.contains(url)); String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem); var pageRoute = MaterialPageRoute( builder: (context) => - ScriptEdit(scriptItem: scriptItem, script: script, url: scriptItem?.url ?? url)); + ScriptEdit(scriptItem: scriptItem, script: script, urls: scriptItem?.urls ?? [url])); if (mounted) Navigator.push(context, pageRoute); }, label: localizations.script, diff --git a/lib/ui/mobile/request/request.dart b/lib/ui/mobile/request/request.dart index d92db0f..fb5d1d8 100644 --- a/lib/ui/mobile/request/request.dart +++ b/lib/ui/mobile/request/request.dart @@ -274,15 +274,15 @@ class RequestRowState extends State { var scriptManager = await ScriptManager.instance; var url = request.domainPath; - var scriptItem = scriptManager.list.firstWhereOrNull((it) => it.url == url); + var scriptItem = scriptManager.list.firstWhereOrNull((it) => it.urls.contains(url)); String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem); var pageRoute = MaterialPageRoute( builder: (context) => ScriptEdit( - scriptItem: scriptItem, - script: script, - url: scriptItem?.url ?? url, - title: request.hostAndPort?.host)); + scriptItem: scriptItem, + script: script, + urls: scriptItem?.urls ?? [url], + title: request.hostAndPort?.host)); Navigator.push(getContext(), pageRoute); }, diff --git a/lib/ui/mobile/setting/script.dart b/lib/ui/mobile/setting/script.dart index d3cacdf..f5eee8f 100644 --- a/lib/ui/mobile/setting/script.dart +++ b/lib/ui/mobile/setting/script.dart @@ -112,13 +112,13 @@ class _MobileScriptState extends State { ])))); } - consoleLog() { + void consoleLog() { // FloatingWindowManager().show(context); Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ScriptConsoleLog())); } //导入js - import() async { + Future import() async { FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.any); if (result == null || result.files.isEmpty) { return; @@ -377,10 +377,10 @@ class _ScriptLogSmallWindowState extends State { class ScriptEdit extends StatefulWidget { final ScriptItem? scriptItem; final String? script; - final String? url; + final List? urls; final String? title; - const ScriptEdit({super.key, this.scriptItem, this.script, this.url, this.title}); + const ScriptEdit({super.key, this.scriptItem, this.script, this.urls, this.title}); @override State createState() => _ScriptEditState(); @@ -389,23 +389,28 @@ class ScriptEdit extends StatefulWidget { class _ScriptEditState extends State { late CodeController script; late TextEditingController nameController; - late TextEditingController urlController; + late List urlControllers; AppLocalizations get localizations => AppLocalizations.of(context)!; @override void initState() { super.initState(); + final urls = + widget.scriptItem?.urls ?? (widget.urls != null && widget.urls!.isNotEmpty ? widget.urls! : []); + urlControllers = + urls.isNotEmpty ? urls.map((u) => TextEditingController(text: u)).toList() : [TextEditingController()]; script = CodeController(language: javascript, text: widget.script ?? ScriptManager.template); nameController = TextEditingController(text: widget.scriptItem?.name ?? widget.title); - urlController = TextEditingController(text: widget.scriptItem?.url ?? widget.url); } @override void dispose() { + for (final c in urlControllers) { + c.dispose(); + } script.dispose(); nameController.dispose(); - urlController.dispose(); super.dispose(); } @@ -437,15 +442,20 @@ class _ScriptEditState extends State { position: FlutterToastr.top); return; } - //新增 + // 收集所有非空、去重的 url + final urls = urlControllers.map((c) => c.text.trim()).where((u) => u.isNotEmpty).toSet().toList(); + if (urls.isEmpty) { + FlutterToastr.show("URL ${localizations.cannotBeEmpty}", context, position: FlutterToastr.top); + return; + } var scriptManager = await ScriptManager.instance; if (widget.scriptItem == null) { - var scriptItem = ScriptItem(true, nameController.text, urlController.text); + var scriptItem = ScriptItem(true, nameController.text, urls); await scriptManager.addScript(scriptItem, script.text); } else { widget.scriptItem?.name = nameController.text; - widget.scriptItem?.url = urlController.text; - widget.scriptItem?.urlReg = null; + widget.scriptItem?.urls = urls; + widget.scriptItem?.urlRegs = null; await scriptManager.updateScript(widget.scriptItem!, script.text); } @@ -464,19 +474,72 @@ class _ScriptEditState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: textField("${localizations.name}:", nameController, localizations.pleaseEnter)), - const SizedBox(height: 10), + const SizedBox(height: 3), Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: textField("URL:", urlController, "github.com/api/*", keyboardType: TextInputType.url)), + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text("URL(s):"), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.add_circle_outline, size: 20), + tooltip: localizations.add, + onPressed: () { + setState(() { + urlControllers.add(TextEditingController()); + }); + }, + ), + ], + ), + ...List.generate( + urlControllers.length, + (i) => Row( + children: [ + Expanded( + child: TextFormField( + controller: urlControllers[i], + validator: (val) => val?.isNotEmpty == true ? null : "", + keyboardType: TextInputType.url, + decoration: InputDecoration( + hintText: "github.com/api/*", + contentPadding: const EdgeInsets.all(10), + errorStyle: const TextStyle(height: 0, fontSize: 0), + focusedBorder: focusedBorder(), + isDense: true, + border: const OutlineInputBorder(), + ), + ), + ), + if (urlControllers.length > 1) + IconButton( + icon: const Icon(Icons.remove_circle_outline, color: Colors.red), + tooltip: localizations.delete, + onPressed: () { + setState(() { + urlControllers[i].dispose(); + urlControllers.removeAt(i); + }); + }, + ), + ], + )), + ], + ), + ), const SizedBox(height: 10), Row(children: [ SizedBox(width: 10), - Text("${localizations.script}:"), + Text( + "${localizations.script}:", + ), SizedBox(width: 10), IconButton( tooltip: localizations.copy, onPressed: () { - //复制 Clipboard.setData(ClipboardData(text: script.text)); FlutterToastr.show(localizations.copied, context, position: FlutterToastr.top); }, @@ -559,7 +622,7 @@ class _ScriptListState extends State { ])))); } - globalMenu() { + Stack globalMenu() { return Stack(children: [ Container( height: 50, @@ -644,14 +707,15 @@ class _ScriptListState extends State { _refreshScript(); }))), const SizedBox(width: 10), - Expanded(child: Text(list[index].url.fixAutoLines(), style: const TextStyle(fontSize: 13))), + Expanded( + child: Text(list[index].urls.join(', ').fixAutoLines(), style: const TextStyle(fontSize: 13))), ], ))); }); } //点击菜单 - showMenus(int index) { + void showMenus(int index) { setState(() { selected.add(index); }); @@ -750,7 +814,7 @@ class _ScriptListState extends State { Share.shareXFiles([file], fileNameOverrides: [fileName], sharePositionOrigin: box?.paintBounds); } - enableStatus(bool enable) { + void enableStatus(bool enable) { for (var idx in selected) { widget.scripts[idx].enabled = enable; } @@ -758,7 +822,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;