mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-03-19 05:19:47 +08:00
887 lines
30 KiB
Dart
887 lines
30 KiB
Dart
/*
|
|
* Copyright 2023 Hongen Wang
|
|
*
|
|
* 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:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:proxypin/l10n/app_localizations.dart';
|
|
import 'package:flutter_toastr/flutter_toastr.dart';
|
|
import 'package:proxypin/network/channel/host_port.dart';
|
|
import 'package:proxypin/network/http/http.dart';
|
|
import 'package:proxypin/network/http/http_headers.dart';
|
|
import 'package:proxypin/network/http/http_client.dart';
|
|
import 'package:proxypin/network/util/logger.dart';
|
|
import 'package:proxypin/ui/component/split_view.dart';
|
|
import 'package:proxypin/ui/component/state_component.dart';
|
|
import 'package:proxypin/ui/configuration.dart';
|
|
import 'package:proxypin/ui/content/body.dart';
|
|
import 'package:proxypin/utils/curl.dart';
|
|
import 'package:proxypin/utils/lang.dart';
|
|
|
|
import '../../component/http_method_popup.dart';
|
|
|
|
enum RequestEditorSource {
|
|
editor,
|
|
breakpointRequest,
|
|
breakpointResponse,
|
|
}
|
|
|
|
/// @author wanghongen
|
|
class RequestEditor extends StatefulWidget {
|
|
final WindowController? windowController;
|
|
final HttpRequest? request;
|
|
final RequestEditorSource source;
|
|
final Function(HttpRequest? request)? onExecuteRequest;
|
|
final Function(HttpResponse? response)? onExecuteResponse;
|
|
final HttpResponse? response;
|
|
|
|
const RequestEditor({
|
|
super.key,
|
|
this.request,
|
|
this.response,
|
|
this.windowController,
|
|
this.source = RequestEditorSource.editor,
|
|
this.onExecuteRequest,
|
|
this.onExecuteResponse,
|
|
});
|
|
|
|
@override
|
|
State<StatefulWidget> createState() {
|
|
return RequestEditorState();
|
|
}
|
|
}
|
|
|
|
class RequestEditorState extends State<RequestEditor> {
|
|
final UrlQueryNotifier _queryNotifier = UrlQueryNotifier();
|
|
final requestLineKey = GlobalKey<_RequestLineState>();
|
|
final requestKey = GlobalKey<_HttpState>();
|
|
final responseKey = GlobalKey<_HttpState>();
|
|
|
|
ValueNotifier<int> responseChange = ValueNotifier<int>(-1);
|
|
HttpRequest? request;
|
|
HttpResponse? response;
|
|
|
|
bool showCURLDialog = false;
|
|
bool executed = false;
|
|
|
|
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
request = widget.request;
|
|
response = widget.response;
|
|
if (response != null) {
|
|
responseChange.value = 1;
|
|
}
|
|
HardwareKeyboard.instance.addHandler(onKeyEvent);
|
|
if (widget.request == null) {
|
|
curlParse();
|
|
}
|
|
}
|
|
|
|
bool onKeyEvent(KeyEvent event) {
|
|
if ((HardwareKeyboard.instance.isMetaPressed ||
|
|
HardwareKeyboard.instance.isControlPressed ||
|
|
HardwareKeyboard.instance.isAltPressed) &&
|
|
event.logicalKey == LogicalKeyboardKey.enter) {
|
|
sendRequest();
|
|
return true;
|
|
}
|
|
|
|
//cmd+w 关闭窗口
|
|
if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&
|
|
event.logicalKey == LogicalKeyboardKey.keyW) {
|
|
HardwareKeyboard.instance.removeHandler(onKeyEvent);
|
|
responseChange.dispose();
|
|
widget.windowController?.close();
|
|
return true;
|
|
}
|
|
|
|
//粘贴
|
|
if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&
|
|
event.logicalKey == LogicalKeyboardKey.keyV) {
|
|
curlParse();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
if ((widget.source == RequestEditorSource.breakpointRequest ||
|
|
widget.source == RequestEditorSource.breakpointResponse) &&
|
|
!executed) {
|
|
if (widget.source == RequestEditorSource.breakpointRequest) {
|
|
widget.onExecuteRequest?.call(null);
|
|
} else {
|
|
widget.onExecuteResponse?.call(null);
|
|
}
|
|
}
|
|
|
|
HardwareKeyboard.instance.removeHandler(onKeyEvent);
|
|
responseChange.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
var title = localizations.httpRequest;
|
|
var buttonText = localizations.send;
|
|
IconData icon = Icons.send;
|
|
|
|
if (widget.source == RequestEditorSource.breakpointRequest) {
|
|
title = "Breakpoint Request";
|
|
buttonText = localizations.execute;
|
|
icon = Icons.play_arrow;
|
|
} else if (widget.source == RequestEditorSource.breakpointResponse) {
|
|
title = "Breakpoint Response";
|
|
buttonText = localizations.execute;
|
|
icon = Icons.play_arrow;
|
|
}
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
|
toolbarHeight: Platform.isWindows ? 36 : null,
|
|
centerTitle: true,
|
|
actions: [
|
|
TextButton.icon(
|
|
onPressed: () async {
|
|
if (widget.source == RequestEditorSource.editor) {
|
|
sendRequest();
|
|
} else {
|
|
executeBreakpoint();
|
|
}
|
|
},
|
|
icon: Icon(icon),
|
|
label: Text(buttonText)),
|
|
if (widget.source == RequestEditorSource.breakpointRequest ||
|
|
widget.source == RequestEditorSource.breakpointResponse)
|
|
TextButton.icon(
|
|
onPressed: () {
|
|
// ignore breakpoint
|
|
if (widget.source == RequestEditorSource.breakpointRequest) {
|
|
widget.onExecuteRequest?.call(null);
|
|
} else {
|
|
widget.onExecuteResponse?.call(null);
|
|
}
|
|
widget.windowController?.close();
|
|
},
|
|
icon: const Icon(Icons.cancel),
|
|
label: Text(localizations.cancel)),
|
|
const SizedBox(width: 10)
|
|
],
|
|
),
|
|
body: Column(children: [
|
|
_RequestLine(key: requestLineKey, request: request, urlQueryNotifier: _queryNotifier),
|
|
Expanded(
|
|
child: VerticalSplitView(
|
|
ratio: 0.53,
|
|
left: _HttpWidget(
|
|
key: requestKey,
|
|
title: const Text("Request", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
|
message: request,
|
|
urlQueryNotifier: _queryNotifier,
|
|
readOnly: widget.source == RequestEditorSource.breakpointResponse,
|
|
),
|
|
right: ValueListenableBuilder(
|
|
valueListenable: responseChange,
|
|
builder: (_, value, __) {
|
|
return Stack(
|
|
children: [
|
|
Offstage(offstage: value != 0, child: const Center(child: CircularProgressIndicator())),
|
|
Offstage(
|
|
offstage: value == 0,
|
|
child: _HttpWidget(
|
|
key: responseKey,
|
|
title: Row(children: [
|
|
const Text("Response", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
|
const Spacer(),
|
|
Text.rich(TextSpan(children: [
|
|
TextSpan(
|
|
text: response?.protocolVersion,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
decorationColor: Colors.green,
|
|
color: Colors.green)),
|
|
WidgetSpan(child: SizedBox(width: 12)),
|
|
TextSpan(
|
|
text: response?.status.code.toString() ?? '',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w500,
|
|
fontSize: 14,
|
|
color: response?.status.isSuccessful() == true ? Colors.green : Colors.red))
|
|
]))
|
|
]),
|
|
message: response,
|
|
readOnly: widget.source != RequestEditorSource.breakpointResponse))
|
|
],
|
|
);
|
|
}),
|
|
)),
|
|
]));
|
|
}
|
|
|
|
///发送请求
|
|
Future<void> sendRequest() async {
|
|
var currentState = requestLineKey.currentState!;
|
|
var headers = requestKey.currentState?.getHeaders();
|
|
var requestBody = requestKey.currentState?.getBody();
|
|
String url = currentState.requestUrl.text;
|
|
HttpRequest request = HttpRequest(currentState.requestMethod, Uri.parse(url).toString(),
|
|
protocolVersion: this.request?.protocolVersion ?? "HTTP/1.1");
|
|
request.headers.addAll(headers);
|
|
request.body = requestBody == null ? null : utf8.encode(requestBody);
|
|
|
|
responseKey.currentState?.change(null);
|
|
responseChange.value = 0;
|
|
|
|
Map? proxyResult = await DesktopMultiWindow.invokeMethod(0, 'getProxyInfo');
|
|
ProxyInfo? proxyInfo = proxyResult == null ? null : ProxyInfo.of(proxyResult['host'], proxyResult['port']);
|
|
|
|
HttpClients.proxyRequest(request, proxyInfo: proxyInfo, timeout: Duration(seconds: 15)).then((response) {
|
|
this.response = response;
|
|
responseKey.currentState?.change(response);
|
|
responseChange.value = 1;
|
|
// if (mounted) FlutterToastr.show(localizations.requestSuccess, context);
|
|
}).catchError((e, stackTrace) {
|
|
logger.e("Request failed", error: e, stackTrace: stackTrace);
|
|
responseChange.value = -1;
|
|
if (mounted) FlutterToastr.show('${localizations.fail}$e', context);
|
|
});
|
|
}
|
|
|
|
void executeBreakpoint() {
|
|
executed = true;
|
|
if (widget.source == RequestEditorSource.breakpointRequest) {
|
|
var currentState = requestLineKey.currentState!;
|
|
var headers = requestKey.currentState?.getHeaders();
|
|
var requestBody = requestKey.currentState?.getBody();
|
|
String url = currentState.requestUrl.text;
|
|
|
|
if (request == null) return;
|
|
HttpRequest newRequest = request!.copy(uri: url);
|
|
newRequest.method = currentState.requestMethod;
|
|
newRequest.headers.clear();
|
|
newRequest.headers.addAll(headers);
|
|
newRequest.body = requestBody == null ? null : utf8.encode(requestBody);
|
|
widget.onExecuteRequest?.call(newRequest);
|
|
} else if (widget.source == RequestEditorSource.breakpointResponse) {
|
|
var headers = responseKey.currentState?.getHeaders();
|
|
var responseBody = responseKey.currentState?.getBody();
|
|
|
|
if (response == null) return;
|
|
HttpResponse newResponse = response!.copy();
|
|
newResponse.headers.clear();
|
|
newResponse.headers.addAll(headers);
|
|
newResponse.body = responseBody == null ? null : utf8.encode(responseBody);
|
|
widget.onExecuteResponse?.call(newResponse);
|
|
}
|
|
}
|
|
|
|
Future<void> curlParse() async {
|
|
var data = await Clipboard.getData('text/plain');
|
|
if (data == null || data.text == null) {
|
|
return;
|
|
}
|
|
|
|
var text = data.text;
|
|
if (text?.startsWith("http://") == true || text?.startsWith("https://") == true) {
|
|
requestLineKey.currentState?.requestUrl.text = text!;
|
|
return;
|
|
}
|
|
|
|
if (text?.trimLeft().startsWith('curl') == true && mounted && !showCURLDialog) {
|
|
showCURLDialog = true;
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text(localizations.prompt),
|
|
content: Text(localizations.curlSchemeRequest),
|
|
actions: [
|
|
TextButton(child: Text(localizations.cancel), onPressed: () => Navigator.of(context).pop()),
|
|
TextButton(
|
|
child: Text(localizations.confirm),
|
|
onPressed: () {
|
|
try {
|
|
setState(() {
|
|
request = Curl.parse(text!);
|
|
requestKey.currentState?.change(request!);
|
|
requestLineKey.currentState?.change(request?.requestUrl, request?.method);
|
|
});
|
|
} catch (e) {
|
|
FlutterToastr.show(localizations.fail, context);
|
|
}
|
|
Navigator.of(context).pop();
|
|
}),
|
|
]);
|
|
},
|
|
).then((value) => showCURLDialog = false);
|
|
}
|
|
}
|
|
}
|
|
|
|
typedef ParamCallback = void Function(String param);
|
|
|
|
class UrlQueryNotifier {
|
|
ParamCallback? _urlNotifier;
|
|
ParamCallback? _paramNotifier;
|
|
|
|
ParamCallback urlListener(ParamCallback listener) => _urlNotifier = listener;
|
|
|
|
ParamCallback paramListener(ParamCallback listener) => _paramNotifier = listener;
|
|
|
|
void onUrlChange(String url) => _urlNotifier?.call(url);
|
|
|
|
void onParamChange(String param) => _paramNotifier?.call(param);
|
|
}
|
|
|
|
class _HttpWidget extends StatefulWidget {
|
|
final HttpMessage? message;
|
|
final bool readOnly;
|
|
final Widget title;
|
|
final UrlQueryNotifier? urlQueryNotifier;
|
|
|
|
const _HttpWidget({this.message, this.readOnly = false, super.key, required this.title, this.urlQueryNotifier});
|
|
|
|
@override
|
|
State<StatefulWidget> createState() {
|
|
return _HttpState();
|
|
}
|
|
}
|
|
|
|
class _HttpState extends State<_HttpWidget> {
|
|
List<String> tabs = ['Header', 'Body'];
|
|
final headerKey = GlobalKey<KeyValState>();
|
|
Map<String, List<String>> initHeader = {};
|
|
HttpMessage? message;
|
|
TextEditingController? body;
|
|
|
|
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
|
|
|
String? getBody() {
|
|
return body?.text;
|
|
}
|
|
|
|
HttpHeaders? getHeaders() {
|
|
return HttpHeaders.fromJson(headerKey.currentState?.getParams() ?? {});
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
if (widget.urlQueryNotifier != null) {
|
|
tabs.insert(0, "URL Params");
|
|
}
|
|
|
|
message = widget.message;
|
|
body = TextEditingController(text: widget.message?.bodyAsString);
|
|
if (widget.message?.headers == null && !widget.readOnly) {
|
|
initHeader["User-Agent"] = ["ProxyPin/${AppConfiguration.version}"];
|
|
initHeader["Accept"] = ["*/*"];
|
|
return;
|
|
}
|
|
}
|
|
|
|
void change(HttpMessage? message) {
|
|
this.message = message;
|
|
body?.text = message?.bodyAsString ?? '';
|
|
headerKey.currentState?.refreshParam(message?.headers.getHeaders());
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (widget.message == null && widget.readOnly) {
|
|
return Scaffold(appBar: AppBar(title: widget.title), body: Center(child: Text(localizations.emptyData)));
|
|
}
|
|
|
|
return SingleChildScrollView(
|
|
child: SizedBox(
|
|
height: MediaQuery.of(context).size.height - 120,
|
|
child: DefaultTabController(
|
|
length: tabs.length,
|
|
initialIndex: tabs.length >= 3 ? 1 : 0,
|
|
child: Scaffold(
|
|
primary: false,
|
|
appBar: PreferredSize(
|
|
preferredSize: const Size.fromHeight(70),
|
|
child: AppBar(
|
|
title: widget.title,
|
|
bottom: TabBar(tabs: tabs.map((e) => Tab(text: e, height: 35)).toList()),
|
|
)),
|
|
body: Padding(
|
|
padding: const EdgeInsets.only(left: 10),
|
|
child: TabBarView(
|
|
children: [
|
|
if (tabs.length == 3)
|
|
KeyValWidget(
|
|
paramNotifier: widget.urlQueryNotifier,
|
|
params: message is HttpRequest
|
|
? (message as HttpRequest).requestUri?.queryParametersAll
|
|
: null),
|
|
KeyValWidget(
|
|
key: headerKey,
|
|
params: message?.headers.getHeaders() ?? initHeader,
|
|
readOnly: widget.readOnly,
|
|
suggestions: HttpHeaders.commonHeaderKeys),
|
|
_body()
|
|
],
|
|
)),
|
|
))));
|
|
}
|
|
|
|
Widget _body() {
|
|
if (widget.readOnly) {
|
|
return KeepAliveWrapper(
|
|
child: SingleChildScrollView(child: HttpBodyWidget(httpMessage: message, hideRequestRewrite: true)));
|
|
}
|
|
|
|
return TextFormField(autofocus: true, controller: body, readOnly: widget.readOnly, minLines: 20, maxLines: 20);
|
|
}
|
|
}
|
|
|
|
///请求行
|
|
class _RequestLine extends StatefulWidget {
|
|
final HttpRequest? request;
|
|
final UrlQueryNotifier? urlQueryNotifier;
|
|
|
|
const _RequestLine({super.key, this.request, this.urlQueryNotifier});
|
|
|
|
@override
|
|
State<StatefulWidget> createState() {
|
|
return _RequestLineState();
|
|
}
|
|
}
|
|
|
|
class _RequestLineState extends State<_RequestLine> {
|
|
HttpMethod requestMethod = HttpMethod.get;
|
|
TextEditingController requestUrl = TextEditingController(text: "");
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
widget.urlQueryNotifier?.paramListener((param) => onQueryChange(param));
|
|
if (widget.request == null) {
|
|
requestUrl.text = 'https://';
|
|
return;
|
|
}
|
|
|
|
var request = widget.request!;
|
|
requestUrl.text = request.requestUrl;
|
|
requestMethod = request.method;
|
|
}
|
|
|
|
@override
|
|
dispose() {
|
|
requestUrl.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void change(String? requestUrl, HttpMethod? requestMethod) {
|
|
this.requestUrl.text = requestUrl ?? this.requestUrl.text;
|
|
this.requestMethod = requestMethod ?? this.requestMethod;
|
|
|
|
urlNotifier();
|
|
}
|
|
|
|
void urlNotifier() {
|
|
var splitFirst = requestUrl.text.splitFirst("?".codeUnits.first);
|
|
widget.urlQueryNotifier?.onUrlChange(splitFirst.length > 1 ? splitFirst.last : '');
|
|
}
|
|
|
|
void onQueryChange(String query) {
|
|
var url = requestUrl.text;
|
|
var indexOf = url.indexOf("?");
|
|
if (indexOf == -1) {
|
|
requestUrl.text = "$url?$query";
|
|
} else {
|
|
requestUrl.text = "${url.substring(0, indexOf)}?$query";
|
|
}
|
|
setState(() {});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return TextField(
|
|
controller: requestUrl,
|
|
decoration: InputDecoration(
|
|
prefix: Padding(
|
|
padding: const EdgeInsets.only(right: 6),
|
|
child: MethodPopupMenu(
|
|
value: requestMethod,
|
|
showSeparator: true,
|
|
onChanged: (val) {
|
|
setState(() => requestMethod = val!);
|
|
},
|
|
),
|
|
),
|
|
isDense: true,
|
|
border: const OutlineInputBorder(borderSide: BorderSide()),
|
|
enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.grey, width: 0.3))),
|
|
onChanged: (value) {
|
|
urlNotifier();
|
|
});
|
|
}
|
|
}
|
|
|
|
class KeyVal {
|
|
bool enabled = true;
|
|
TextEditingController key;
|
|
TextEditingController value;
|
|
FocusNode? keyFocusNode;
|
|
FocusNode? valueFocusNode;
|
|
|
|
KeyVal(this.key, this.value);
|
|
}
|
|
|
|
///key value
|
|
class KeyValWidget extends StatefulWidget {
|
|
final Map<String, List<String>>? params;
|
|
final bool readOnly; //只读
|
|
final UrlQueryNotifier? paramNotifier;
|
|
final List<String>? suggestions;
|
|
|
|
const KeyValWidget({super.key, this.params, this.readOnly = false, this.paramNotifier, this.suggestions});
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => KeyValState();
|
|
}
|
|
|
|
class KeyValState extends State<KeyValWidget> with AutomaticKeepAliveClientMixin {
|
|
final List<KeyVal> _params = [];
|
|
|
|
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
widget.paramNotifier?.urlListener((url) => onChange(url));
|
|
if (widget.params == null) {
|
|
var keyVal = KeyVal(TextEditingController(), TextEditingController());
|
|
_params.add(keyVal);
|
|
return;
|
|
}
|
|
|
|
widget.params?.forEach((name, values) {
|
|
for (var val in values) {
|
|
var keyVal = KeyVal(TextEditingController(text: name), TextEditingController(text: val));
|
|
_params.add(keyVal);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
dispose() {
|
|
clear();
|
|
super.dispose();
|
|
}
|
|
|
|
//监听url发生变化 更改表单
|
|
void onChange(String value) {
|
|
var query = value.split("&");
|
|
int index = 0;
|
|
while (index < query.length) {
|
|
var splitFirst = query[index].splitFirst('='.codeUnits.first);
|
|
String key = splitFirst.first;
|
|
String? val = splitFirst.length == 1 ? null : splitFirst.last;
|
|
if (_params.length <= index) {
|
|
_params.add(KeyVal(TextEditingController(text: key), TextEditingController(text: val)));
|
|
continue;
|
|
}
|
|
|
|
var keyVal = _params[index++];
|
|
keyVal.key.text = key;
|
|
keyVal.value.text = val ?? '';
|
|
}
|
|
|
|
_params.length = index;
|
|
setState(() {});
|
|
}
|
|
|
|
void notifierChange() {
|
|
if (widget.paramNotifier == null) return;
|
|
String query = _params
|
|
.where((e) => e.enabled && e.key.text.isNotEmpty)
|
|
.map((e) => "${e.key.text}=${e.value.text}".replaceAll("&", "%26"))
|
|
.join("&");
|
|
widget.paramNotifier?.onParamChange(query);
|
|
}
|
|
|
|
void clear() {
|
|
for (var element in _params) {
|
|
element.key.dispose();
|
|
element.value.dispose();
|
|
}
|
|
_params.clear();
|
|
}
|
|
|
|
//刷新param
|
|
void refreshParam(Map<String, List<String>>? headers) {
|
|
clear();
|
|
setState(() {
|
|
headers?.forEach((name, values) {
|
|
for (var val in values) {
|
|
var keyVal = KeyVal(TextEditingController(text: name), TextEditingController(text: val));
|
|
_params.add(keyVal);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
///获取所有请求头
|
|
Map<String, List<String>> getParams() {
|
|
Map<String, List<String>> map = {};
|
|
for (var keVal in _params) {
|
|
if (keVal.key.text.isEmpty || !keVal.enabled) {
|
|
continue;
|
|
}
|
|
map[keVal.key.text] ??= [];
|
|
map[keVal.key.text]!.add(keVal.value.text);
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
|
|
var list = [
|
|
const Row(children: [
|
|
SizedBox(width: 38),
|
|
Expanded(flex: 4, child: Text('Key')),
|
|
Expanded(flex: 5, child: Text('Value'))
|
|
]),
|
|
..._buildRows(),
|
|
];
|
|
|
|
if (!widget.readOnly) {
|
|
list.add(TextButton(
|
|
child: Text(localizations.add, textAlign: TextAlign.center),
|
|
onPressed: () {
|
|
setState(() {
|
|
_params.add(KeyVal(TextEditingController(), TextEditingController()));
|
|
});
|
|
},
|
|
));
|
|
}
|
|
return Scaffold(
|
|
body: Padding(
|
|
padding: const EdgeInsets.only(top: 10),
|
|
child: ListView.separated(
|
|
separatorBuilder: (context, index) =>
|
|
index == list.length ? const SizedBox() : const Divider(thickness: 0.2),
|
|
itemBuilder: (context, index) => list[index],
|
|
itemCount: list.length)));
|
|
}
|
|
|
|
List<Widget> _buildRows() {
|
|
List<Widget> list = [];
|
|
for (var keyVal in _params) {
|
|
list.add(_row(
|
|
keyVal,
|
|
widget.readOnly
|
|
? null
|
|
: Padding(
|
|
padding: const EdgeInsets.only(right: 15),
|
|
child: InkWell(
|
|
onTap: () {
|
|
setState(() {
|
|
_params.remove(keyVal);
|
|
keyVal.key.dispose();
|
|
keyVal.value.dispose();
|
|
});
|
|
notifierChange();
|
|
},
|
|
child: const Icon(Icons.remove_circle, size: 16)))));
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
Widget _cell(KeyVal keyVal,
|
|
{bool isKey = false,
|
|
FocusNode? focusNode,
|
|
List<String>? suggestions,
|
|
Map<String, List<String>>? valueSuggestions}) {
|
|
TextEditingController textController = isKey ? keyVal.key : keyVal.value;
|
|
|
|
if (!widget.readOnly && (suggestions != null || valueSuggestions != null)) {
|
|
return Container(
|
|
padding: const EdgeInsets.only(right: 5),
|
|
child: RawAutocomplete<String>(
|
|
textEditingController: textController,
|
|
focusNode: focusNode,
|
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
if (textEditingValue.text.isEmpty) {
|
|
return const Iterable<String>.empty();
|
|
}
|
|
|
|
var currentSuggestions = suggestions;
|
|
if (!isKey && valueSuggestions?.containsKey(keyVal.key.text) == true) {
|
|
currentSuggestions = valueSuggestions![keyVal.key.text];
|
|
}
|
|
|
|
if (currentSuggestions == null) {
|
|
return const Iterable<String>.empty();
|
|
}
|
|
|
|
return currentSuggestions.where((String option) {
|
|
return option.toLowerCase().contains(textEditingValue.text.toLowerCase());
|
|
});
|
|
},
|
|
onSelected: (String selection) {
|
|
textController.text = selection;
|
|
notifierChange();
|
|
},
|
|
fieldViewBuilder: (BuildContext context, TextEditingController textEditingController,
|
|
FocusNode fieldFocusNode, VoidCallback onFieldSubmitted) {
|
|
return TextFormField(
|
|
controller: textEditingController,
|
|
focusNode: fieldFocusNode,
|
|
onFieldSubmitted: (String value) {
|
|
onFieldSubmitted();
|
|
},
|
|
onChanged: (val) {
|
|
if (isKey) setState(() {});
|
|
notifierChange();
|
|
},
|
|
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
|
|
minLines: 1,
|
|
maxLines: 3,
|
|
decoration: InputDecoration(
|
|
isDense: true,
|
|
hintStyle: const TextStyle(color: Colors.grey),
|
|
contentPadding: const EdgeInsets.fromLTRB(5, 13, 5, 13),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 1.5)),
|
|
border: InputBorder.none,
|
|
hintText: isKey ? "Key" : "Value"));
|
|
},
|
|
optionsViewBuilder:
|
|
(BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) {
|
|
return Align(
|
|
alignment: Alignment.topLeft,
|
|
child: Material(
|
|
elevation: 4.0,
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxHeight: 200, maxWidth: 300),
|
|
child: ListView.builder(
|
|
padding: EdgeInsets.zero,
|
|
shrinkWrap: true,
|
|
itemCount: options.length,
|
|
itemBuilder: (BuildContext context, int index) {
|
|
final String option = options.elementAt(index);
|
|
return InkWell(
|
|
onTap: () {
|
|
onSelected(option);
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.all(10.0),
|
|
child: _buildHighlightText(option, textController.text),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
));
|
|
}
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.only(right: 5),
|
|
child: TextFormField(
|
|
readOnly: widget.readOnly,
|
|
style: TextStyle(fontSize: 13, fontWeight: isKey ? FontWeight.w500 : null),
|
|
controller: textController,
|
|
onChanged: (val) => notifierChange(),
|
|
minLines: 1,
|
|
maxLines: 3,
|
|
decoration: InputDecoration(
|
|
isDense: true,
|
|
hintStyle: const TextStyle(color: Colors.grey),
|
|
contentPadding: const EdgeInsets.fromLTRB(5, 13, 5, 13),
|
|
focusedBorder: widget.readOnly
|
|
? null
|
|
: OutlineInputBorder(
|
|
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 1.5)),
|
|
border: InputBorder.none,
|
|
hintText: isKey ? "Key" : "Value")));
|
|
}
|
|
|
|
Widget _row(KeyVal keyVal, Widget? op) {
|
|
if (widget.suggestions != null) {
|
|
keyVal.keyFocusNode ??= FocusNode();
|
|
}
|
|
|
|
Map<String, List<String>>? valueSuggestions;
|
|
if (widget.suggestions != null) {
|
|
keyVal.valueFocusNode ??= FocusNode();
|
|
valueSuggestions = HttpHeaders.commonHeaderValues;
|
|
}
|
|
|
|
return Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
|
|
if (op != null)
|
|
Checkbox(
|
|
value: keyVal.enabled,
|
|
onChanged: (val) {
|
|
setState(() {
|
|
keyVal.enabled = val!;
|
|
});
|
|
notifierChange();
|
|
}),
|
|
Container(width: 5),
|
|
Expanded(
|
|
flex: 4, child: _cell(keyVal, isKey: true, suggestions: widget.suggestions, focusNode: keyVal.keyFocusNode)),
|
|
const Text(":", style: TextStyle(color: Colors.deepOrangeAccent)),
|
|
const SizedBox(width: 8),
|
|
Expanded(flex: 6, child: _cell(keyVal, focusNode: keyVal.valueFocusNode, valueSuggestions: valueSuggestions)),
|
|
op ?? const SizedBox()
|
|
]);
|
|
}
|
|
|
|
Widget _buildHighlightText(String text, String query) {
|
|
if (query.isEmpty) {
|
|
return Text(text);
|
|
}
|
|
|
|
int index = text.toLowerCase().indexOf(query.toLowerCase());
|
|
if (index < 0) {
|
|
return Text(text);
|
|
}
|
|
|
|
return Text.rich(TextSpan(children: [
|
|
TextSpan(text: text.substring(0, index)),
|
|
TextSpan(
|
|
text: text.substring(index, index + query.length),
|
|
style: TextStyle(color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.bold)),
|
|
TextSpan(text: text.substring(index + query.length))
|
|
]));
|
|
}
|
|
}
|