mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-03-19 05:19:47 +08:00
Add autocomplete suggestions for HTTP headers in request editor
This commit is contained in:
@@ -27,6 +27,84 @@ class HttpHeaders {
|
||||
static const String Cookie = "Cookie";
|
||||
static const String PROXY_AUTHORIZATION = "Proxy-Authorization";
|
||||
|
||||
static const List<String> commonHeaderKeys = [
|
||||
'Accept',
|
||||
'Accept-Charset',
|
||||
'Accept-Encoding',
|
||||
'Accept-Language',
|
||||
'Accept-Ranges',
|
||||
'Authorization',
|
||||
'Cache-Control',
|
||||
'Connection',
|
||||
'Content-Type',
|
||||
'Content-Length',
|
||||
'Content-Encoding',
|
||||
'Cookie',
|
||||
'Date',
|
||||
'Expect',
|
||||
'From',
|
||||
'Host',
|
||||
'If-Match',
|
||||
'If-Modified-Since',
|
||||
'If-None-Match',
|
||||
'If-Range',
|
||||
'If-Unmodified-Since',
|
||||
'Max-Forwards',
|
||||
'Origin',
|
||||
'Pragma',
|
||||
'Proxy-Authorization',
|
||||
'Range',
|
||||
'Referer',
|
||||
'TE',
|
||||
'Upgrade',
|
||||
'User-Agent',
|
||||
'Via',
|
||||
'Warning',
|
||||
'X-Requested-With',
|
||||
'DNT',
|
||||
'X-Forwarded-For',
|
||||
'X-Forwarded-Host',
|
||||
'X-Forwarded-Proto',
|
||||
'Front-End-Https',
|
||||
'X-Http-Method-Override',
|
||||
'X-ATT-DeviceId',
|
||||
'X-Wap-Profile',
|
||||
'Proxy-Connection',
|
||||
'X-UIDH',
|
||||
'X-Csrf-Token',
|
||||
'X-Request-ID',
|
||||
'X-Correlation-ID',
|
||||
'Save-Data'
|
||||
];
|
||||
|
||||
static const Map<String, List<String>> commonHeaderValues = {
|
||||
'Accept': [
|
||||
'application/json, text/plain, */*',
|
||||
'application/xml, text/xml, */*',
|
||||
'text/html, application/xhtml+xml, */*',
|
||||
'*/*'
|
||||
],
|
||||
'Accept-Charset': ['utf-8, iso-8859-1;q=0.5', 'utf-8'],
|
||||
'Accept-Encoding': ['gzip, deflate, br', 'gzip, deflate'],
|
||||
'Accept-Language': ['en-US,en;q=0.9', 'zh-CN,zh;q=0.9'],
|
||||
'Cache-Control': ['no-cache', 'max-age=0', 'no-store'],
|
||||
'Connection': ['keep-alive', 'close'],
|
||||
'Content-Type': [
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded',
|
||||
'multipart/form-data',
|
||||
'text/plain',
|
||||
'text/html',
|
||||
'application/xml'
|
||||
],
|
||||
'User-Agent': [
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36',
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1',
|
||||
'Mozilla/5.0 (Android 11; Mobile; rv:68.0) Gecko/68.0 Firefox/68.0'
|
||||
],
|
||||
};
|
||||
|
||||
final LinkedHashMap<String, List<String>> _headers = LinkedHashMap<String, List<String>>();
|
||||
|
||||
// 由小写标头名称键入的原始标头名称。
|
||||
|
||||
@@ -208,7 +208,7 @@ class RequestEditorState extends State<RequestEditor> {
|
||||
]))
|
||||
]),
|
||||
message: response,
|
||||
readOnly: widget.source == RequestEditorSource.breakpointRequest))
|
||||
readOnly: widget.source != RequestEditorSource.breakpointResponse))
|
||||
],
|
||||
);
|
||||
}),
|
||||
@@ -377,7 +377,7 @@ class _HttpState extends State<_HttpWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
change(HttpMessage? message) {
|
||||
void change(HttpMessage? message) {
|
||||
this.message = message;
|
||||
body?.text = message?.bodyAsString ?? '';
|
||||
headerKey.currentState?.refreshParam(message?.headers.getHeaders());
|
||||
@@ -416,7 +416,8 @@ class _HttpState extends State<_HttpWidget> {
|
||||
KeyValWidget(
|
||||
key: headerKey,
|
||||
params: message?.headers.getHeaders() ?? initHeader,
|
||||
readOnly: widget.readOnly),
|
||||
readOnly: widget.readOnly,
|
||||
suggestions: HttpHeaders.commonHeaderKeys),
|
||||
_body()
|
||||
],
|
||||
)),
|
||||
@@ -521,6 +522,8 @@ class KeyVal {
|
||||
bool enabled = true;
|
||||
TextEditingController key;
|
||||
TextEditingController value;
|
||||
FocusNode? keyFocusNode;
|
||||
FocusNode? valueFocusNode;
|
||||
|
||||
KeyVal(this.key, this.value);
|
||||
}
|
||||
@@ -530,8 +533,9 @@ 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});
|
||||
const KeyValWidget({super.key, this.params, this.readOnly = false, this.paramNotifier, this.suggestions});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => KeyValState();
|
||||
@@ -570,7 +574,7 @@ class KeyValState extends State<KeyValWidget> with AutomaticKeepAliveClientMixin
|
||||
}
|
||||
|
||||
//监听url发生变化 更改表单
|
||||
onChange(String value) {
|
||||
void onChange(String value) {
|
||||
var query = value.split("&");
|
||||
int index = 0;
|
||||
while (index < query.length) {
|
||||
@@ -591,7 +595,7 @@ class KeyValState extends State<KeyValWidget> with AutomaticKeepAliveClientMixin
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
notifierChange() {
|
||||
void notifierChange() {
|
||||
if (widget.paramNotifier == null) return;
|
||||
String query = _params
|
||||
.where((e) => e.enabled && e.key.text.isNotEmpty)
|
||||
@@ -600,7 +604,7 @@ class KeyValState extends State<KeyValWidget> with AutomaticKeepAliveClientMixin
|
||||
widget.paramNotifier?.onParamChange(query);
|
||||
}
|
||||
|
||||
clear() {
|
||||
void clear() {
|
||||
for (var element in _params) {
|
||||
element.key.dispose();
|
||||
element.value.dispose();
|
||||
@@ -609,7 +613,7 @@ class KeyValState extends State<KeyValWidget> with AutomaticKeepAliveClientMixin
|
||||
}
|
||||
|
||||
//刷新param
|
||||
refreshParam(Map<String, List<String>>? headers) {
|
||||
void refreshParam(Map<String, List<String>>? headers) {
|
||||
clear();
|
||||
setState(() {
|
||||
headers?.forEach((name, values) {
|
||||
@@ -692,13 +696,103 @@ class KeyValState extends State<KeyValWidget> with AutomaticKeepAliveClientMixin
|
||||
return list;
|
||||
}
|
||||
|
||||
Widget _cell(TextEditingController val, {bool isKey = false}) {
|
||||
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: val,
|
||||
controller: textController,
|
||||
onChanged: (val) => notifierChange(),
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
@@ -715,6 +809,16 @@ class KeyValState extends State<KeyValWidget> with AutomaticKeepAliveClientMixin
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -726,11 +830,31 @@ class KeyValState extends State<KeyValWidget> with AutomaticKeepAliveClientMixin
|
||||
notifierChange();
|
||||
}),
|
||||
Container(width: 5),
|
||||
Expanded(flex: 4, child: _cell(keyVal.key, isKey: true)),
|
||||
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.value)),
|
||||
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))
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,7 +621,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MARKETING_VERSION = 1.1.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -762,7 +762,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MARKETING_VERSION = 1.1.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -791,7 +791,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MARKETING_VERSION = 1.1.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
Reference in New Issue
Block a user