mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-06-01 17:15:48 +08:00
Add autocomplete suggestions for HTTP headers in request editor
This commit is contained in:
@@ -34,7 +34,6 @@ import 'package:proxypin/ui/mobile/request/request_editor_source.dart';
|
||||
|
||||
import '../../component/http_method_popup.dart';
|
||||
|
||||
|
||||
/// @author wanghongen
|
||||
class MobileRequestEditor extends StatefulWidget {
|
||||
final HttpRequest? request;
|
||||
@@ -92,7 +91,10 @@ class RequestEditorState extends State<MobileRequestEditor> with SingleTickerPro
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
tabController = TabController(length: tabs.length, vsync: this, initialIndex: widget.source == RequestEditorSource.breakpointResponse ? 1 : 0);
|
||||
tabController = TabController(
|
||||
length: tabs.length,
|
||||
vsync: this,
|
||||
initialIndex: widget.source == RequestEditorSource.breakpointResponse ? 1 : 0);
|
||||
request = widget.request;
|
||||
response = widget.response;
|
||||
if (widget.request == null) {
|
||||
@@ -210,7 +212,7 @@ class RequestEditorState extends State<MobileRequestEditor> with SingleTickerPro
|
||||
style: TextStyle(
|
||||
color: response?.status.isSuccessful() == true ? Colors.blue : Colors.red))
|
||||
]),
|
||||
readOnly: widget.source == RequestEditorSource.breakpointRequest,
|
||||
readOnly: widget.source != RequestEditorSource.breakpointResponse,
|
||||
message: response);
|
||||
}),
|
||||
],
|
||||
@@ -333,7 +335,7 @@ class _HttpState extends State<_HttpWidget> with AutomaticKeepAliveClientMixin {
|
||||
}
|
||||
}
|
||||
|
||||
change(HttpMessage? message) {
|
||||
void change(HttpMessage? message) {
|
||||
this.message = message;
|
||||
body = message?.bodyAsString;
|
||||
headerKey.currentState?.refreshParam(message?.headers.getHeaders());
|
||||
@@ -367,6 +369,7 @@ class _HttpState extends State<_HttpWidget> with AutomaticKeepAliveClientMixin {
|
||||
title: "Headers",
|
||||
params: message?.headers.getHeaders() ?? initHeader,
|
||||
key: headerKey,
|
||||
suggestions: HttpHeaders.commonHeaderKeys,
|
||||
readOnly: widget.readOnly),
|
||||
// 请求头
|
||||
const SizedBox(height: 10),
|
||||
@@ -492,9 +495,16 @@ class KeyValWidget extends StatefulWidget {
|
||||
final bool readOnly; //只读
|
||||
final UrlQueryNotifier? paramNotifier;
|
||||
final bool expanded;
|
||||
final List<String>? suggestions;
|
||||
|
||||
const KeyValWidget(
|
||||
{super.key, this.params, this.readOnly = false, this.paramNotifier, required this.title, this.expanded = true});
|
||||
{super.key,
|
||||
this.params,
|
||||
this.readOnly = false,
|
||||
this.paramNotifier,
|
||||
required this.title,
|
||||
this.expanded = true,
|
||||
this.suggestions});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
@@ -523,7 +533,7 @@ class KeyValState extends State<KeyValWidget> {
|
||||
}
|
||||
|
||||
//监听url发生变化 更改表单
|
||||
onChange(String value) {
|
||||
void onChange(String value) {
|
||||
var query = value.split("&");
|
||||
int index = 0;
|
||||
while (index < query.length) {
|
||||
@@ -544,7 +554,7 @@ class KeyValState extends State<KeyValWidget> {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
notifierChange() {
|
||||
void notifierChange() {
|
||||
if (widget.paramNotifier == null) return;
|
||||
String query = _params
|
||||
.where((e) => e.enabled && e.key.isNotEmpty)
|
||||
@@ -568,7 +578,7 @@ class KeyValState extends State<KeyValWidget> {
|
||||
}
|
||||
|
||||
//刷新param
|
||||
refreshParam(Map<String, List<String>>? headers) {
|
||||
void refreshParam(Map<String, List<String>>? headers) {
|
||||
_params.clear();
|
||||
setState(() {
|
||||
headers?.forEach((name, values) {
|
||||
@@ -630,7 +640,7 @@ class KeyValState extends State<KeyValWidget> {
|
||||
}
|
||||
|
||||
/// 修改请求头
|
||||
modifyParam(KeyVal keyVal) {
|
||||
void modifyParam(KeyVal keyVal) {
|
||||
//隐藏输入框焦点
|
||||
hideKeyword(context);
|
||||
String headerName = keyVal.key;
|
||||
@@ -638,42 +648,164 @@ class KeyValState extends State<KeyValWidget> {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
return AlertDialog(
|
||||
titlePadding: const EdgeInsets.only(left: 25, top: 10),
|
||||
actionsPadding: const EdgeInsets.only(right: 10, bottom: 10),
|
||||
title: Text(localizations.modifyRequestHeader, style: const TextStyle(fontSize: 18)),
|
||||
content: Wrap(
|
||||
children: [
|
||||
TextFormField(
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
initialValue: headerName,
|
||||
decoration: InputDecoration(labelText: localizations.headerName),
|
||||
onChanged: (value) => headerName = value,
|
||||
),
|
||||
TextFormField(
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
initialValue: val,
|
||||
decoration: InputDecoration(labelText: localizations.value),
|
||||
onChanged: (value) => val = value,
|
||||
)
|
||||
return StatefulBuilder(builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
titlePadding: const EdgeInsets.only(left: 25, top: 10),
|
||||
actionsPadding: const EdgeInsets.only(right: 10, bottom: 10),
|
||||
title: Text(localizations.modifyRequestHeader, style: const TextStyle(fontSize: 18)),
|
||||
content: Wrap(
|
||||
children: [
|
||||
if (widget.suggestions != null)
|
||||
Autocomplete<String>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
if (textEditingValue.text.isEmpty) {
|
||||
return const Iterable<String>.empty();
|
||||
}
|
||||
return widget.suggestions!.where((String option) {
|
||||
return option.toLowerCase().contains(textEditingValue.text.toLowerCase());
|
||||
});
|
||||
},
|
||||
onSelected: (String selection) {
|
||||
setState(() {
|
||||
headerName = selection;
|
||||
});
|
||||
},
|
||||
fieldViewBuilder: (BuildContext context, TextEditingController textEditingController,
|
||||
FocusNode focusNode, VoidCallback onFieldSubmitted) {
|
||||
return TextFormField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(labelText: localizations.headerName),
|
||||
onChanged: (value) {
|
||||
headerName = value;
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
},
|
||||
initialValue: TextEditingValue(text: headerName),
|
||||
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, headerName),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
else
|
||||
TextFormField(
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
initialValue: headerName,
|
||||
decoration: InputDecoration(labelText: localizations.headerName),
|
||||
onChanged: (value) {
|
||||
headerName = value;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (HttpHeaders.commonHeaderValues.containsKey(headerName))
|
||||
Autocomplete<String>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
if (textEditingValue.text.isEmpty) {
|
||||
return const Iterable<String>.empty();
|
||||
}
|
||||
return HttpHeaders.commonHeaderValues[headerName]!.where((String option) {
|
||||
return option.toLowerCase().contains(textEditingValue.text.toLowerCase());
|
||||
});
|
||||
},
|
||||
onSelected: (String selection) {
|
||||
val = selection;
|
||||
},
|
||||
fieldViewBuilder: (BuildContext context, TextEditingController textEditingController,
|
||||
FocusNode focusNode, VoidCallback onFieldSubmitted) {
|
||||
return TextFormField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
decoration: InputDecoration(labelText: localizations.value),
|
||||
onChanged: (value) => val = value,
|
||||
);
|
||||
},
|
||||
initialValue: TextEditingValue(text: val),
|
||||
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, val),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
else
|
||||
TextFormField(
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
initialValue: val,
|
||||
decoration: InputDecoration(labelText: localizations.value),
|
||||
onChanged: (value) => val = value,
|
||||
)
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(localizations.cancel)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
this.setState(() {
|
||||
keyVal.key = headerName;
|
||||
keyVal.value = val;
|
||||
});
|
||||
notifierChange();
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
child: Text(localizations.modify)),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(localizations.cancel)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
keyVal.key = headerName;
|
||||
keyVal.value = val;
|
||||
});
|
||||
notifierChange();
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
child: Text(localizations.modify)),
|
||||
],
|
||||
);
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -718,4 +850,23 @@ class KeyValState extends State<KeyValWidget> {
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
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))
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user