From a950c71d8194ba69f5ce37b5979b35b99b396831 Mon Sep 17 00:00:00 2001 From: wanghongenpin Date: Sun, 10 May 2026 21:03:41 +0800 Subject: [PATCH] feat: add 'Copy as fetch' functionality (#685) --- lib/l10n/app_en.arb | 1 + lib/l10n/app_localizations.dart | 6 ++++++ lib/l10n/app_localizations_en.dart | 3 +++ lib/l10n/app_localizations_zh.dart | 6 ++++++ lib/l10n/app_zh.arb | 1 + lib/l10n/app_zh_Hant.arb | 1 + lib/ui/content/menu.dart | 27 +++++++++++++++++++++------ lib/ui/desktop/request/request.dart | 7 +++++++ lib/ui/mobile/mobile.dart | 7 +++++-- lib/utils/curl.dart | 28 ++++++++++++++++++++++++++++ 10 files changed, 79 insertions(+), 8 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6faa962..dbcc221 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -159,6 +159,7 @@ "copyRequestResponse": "Copy Request and Response", "copyCurl": "Copy cURL", "copyAsPythonRequests": "Copy as Python Requests", + "copyAsFetch": "Copy as fetch", "delete": "Delete", "rename": "Rename", "repeat": "Repeat", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 56a580f..9b82d15 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1014,6 +1014,12 @@ abstract class AppLocalizations { /// **'Copy as Python Requests'** String get copyAsPythonRequests; + /// No description provided for @copyAsFetch. + /// + /// In en, this message translates to: + /// **'Copy as fetch'** + String get copyAsFetch; + /// No description provided for @delete. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index eedf5e0..edf5a7d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -473,6 +473,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get copyAsPythonRequests => 'Copy as Python Requests'; + @override + String get copyAsFetch => 'Copy as fetch'; + @override String get delete => 'Delete'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 812bc60..9efa432 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -471,6 +471,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get copyAsPythonRequests => '复制 Python Requests'; + @override + String get copyAsFetch => '复制 fetch'; + @override String get delete => '删除'; @@ -1559,6 +1562,9 @@ class AppLocalizationsZhHant extends AppLocalizationsZh { @override String get copyAsPythonRequests => '複製 Python Requests'; + @override + String get copyAsFetch => '複製 fetch'; + @override String get delete => '刪除'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 3843611..27a687c 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -160,6 +160,7 @@ "copyRequestResponse": "复制 请求和响应", "copyCurl": "复制 cURL", "copyAsPythonRequests": "复制 Python Requests", + "copyAsFetch": "复制 fetch", "delete": "删除", "rename": "重命名", "repeat": "重放", diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb index 8080d7a..f5d7635 100644 --- a/lib/l10n/app_zh_Hant.arb +++ b/lib/l10n/app_zh_Hant.arb @@ -152,6 +152,7 @@ "copyRequestResponse": "複製 請求和回應", "copyCurl": "複製 cURL", "copyAsPythonRequests": "複製 Python Requests", + "copyAsFetch": "複製 fetch", "delete": "刪除", "rename": "重新命名", "repeat": "重放", diff --git a/lib/ui/content/menu.dart b/lib/ui/content/menu.dart index 6ea8e43..783ac02 100644 --- a/lib/ui/content/menu.dart +++ b/lib/ui/content/menu.dart @@ -45,8 +45,10 @@ class ShareWidget extends StatelessWidget { FlutterToastr.show(localizations.emptyData, context); return; } - Share.share(request!.requestUrl, - subject: localizations.proxyPinSoftware, sharePositionOrigin: await _sharePositionOrigin(context)); + SharePlus.instance.share(ShareParams( + text: request!.requestUrl, + subject: localizations.proxyPinSoftware, + sharePositionOrigin: await _sharePositionOrigin(context))); }, ), PopupMenuItem( @@ -60,10 +62,11 @@ class ShareWidget extends StatelessWidget { var file = XFile.fromData(utf8.encode(copyRequest(request!, response)), name: localizations.captureDetail, mimeType: "txt"); - Share.shareXFiles([file], + SharePlus.instance.share(ShareParams( + files: [file], fileNameOverrides: ['request.txt'], text: localizations.proxyPinSoftware, - sharePositionOrigin: await _sharePositionOrigin(context)); + sharePositionOrigin: await _sharePositionOrigin(context))); }), PopupMenuItem( padding: const EdgeInsets.only(left: 10, right: 2), @@ -75,10 +78,22 @@ class ShareWidget extends StatelessWidget { var text = curlRequest(request!); var file = XFile.fromData(utf8.encode(text), name: "cURL.txt", mimeType: "txt"); - Share.shareXFiles([file], + SharePlus.instance.share(ShareParams( + files: [file], fileNameOverrides: ["cURL.txt"], text: localizations.proxyPinSoftware, - sharePositionOrigin: await _sharePositionOrigin(context)); + sharePositionOrigin: await _sharePositionOrigin(context))); + }), + PopupMenuItem( + padding: const EdgeInsets.only(left: 10, right: 2), + child: Text("${localizations.share} Fetch API"), + onTap: () async { + if (request == null) { + return; + } + var text = copyAsFetch(request!); + SharePlus.instance + .share(ShareParams(text: text, sharePositionOrigin: await _sharePositionOrigin(context))); }), PopupMenuItem( enabled: QuickShareService.isRemoteConnected(proxyServer), diff --git a/lib/ui/desktop/request/request.dart b/lib/ui/desktop/request/request.dart index afe489c..9c5b76d 100644 --- a/lib/ui/desktop/request/request.dart +++ b/lib/ui/desktop/request/request.dart @@ -204,6 +204,13 @@ class _RequestWidgetState extends State { .then((value) => FlutterToastr.show(localizations.copied, rootNavigator: true, context)); }, ), + MenuItem( + label: localizations.copyAsFetch, + onClick: (_) { + Clipboard.setData(ClipboardData(text: copyAsFetch(widget.request))) + .then((value) => FlutterToastr.show(localizations.copied, rootNavigator: true, context)); + }, + ), ])), MenuItem.separator(), MenuItem( diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index d8559dd..f4a8ac6 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -546,8 +546,11 @@ class _MobileAppBar extends StatelessWidget implements PreferredSizeWidget { return AppBar( leading: bottomNavigation ? const SizedBox() : null, - systemOverlayStyle: - Platform.isAndroid ? SystemUiOverlayStyle(systemNavigationBarColor: ColorScheme.of(context).surface) : null, + systemOverlayStyle: Platform.isAndroid + ? SystemUiOverlayStyle( + systemNavigationBarColor: ColorScheme.of(context).surface, + statusBarColor: ColorScheme.of(context).surface) + : null, title: MobileSearch( key: MobileApp.searchStateKey, onSearch: (val) => MobileApp.requestStateKey.currentState?.search(val)), actions: [ diff --git a/lib/utils/curl.dart b/lib/utils/curl.dart index c2c42e4..426dce4 100644 --- a/lib/utils/curl.dart +++ b/lib/utils/curl.dart @@ -17,6 +17,34 @@ import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/http_headers.dart'; import 'package:proxypin/utils/lang.dart'; +import 'dart:convert'; + +/// 复制为 fetch 请求 +String copyAsFetch(HttpRequest request) { + final headers = request.headers.entries + .where((entry) => entry.key.toLowerCase() != 'content-length') + .toList(); + + final sb = StringBuffer(); + sb.writeln('fetch(${jsonEncode(request.requestUrl)}, {'); + sb.writeln(' method: ${jsonEncode(request.method.name.toUpperCase())},'); + + if (headers.isNotEmpty) { + sb.writeln(' headers: {'); + for (final entry in headers) { + sb.writeln(' ${jsonEncode(entry.key)}: ${jsonEncode(entry.value)},'); + } + sb.writeln(' },'); + } + + if (request.bodyAsString.isNotEmpty) { + sb.writeln(' body: ${jsonEncode(request.bodyAsString)},'); + } + + sb.writeln('});'); + return sb.toString(); +} + ///复制cURL请求 String curlRequest(HttpRequest request) {