Add autocomplete suggestions for HTTP headers in request editor

This commit is contained in:
wanghongenpin
2026-02-27 01:00:03 +08:00
parent 905d8932bd
commit 6bf5063bed
4 changed files with 412 additions and 59 deletions

View File

@@ -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))
]));
}
}