mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-03-15 04:23:17 +08:00
The script supports multiple URL matching
This commit is contained in:
@@ -268,7 +268,7 @@ class Client extends Network {
|
||||
// host = host.substring(1, host.length - 1);
|
||||
// }
|
||||
|
||||
logger.d('Connecting to $host:${hostAndPort.port}');
|
||||
// logger.d('Connecting to $host:${hostAndPort.port}');
|
||||
return Socket.connect(host, hostAndPort.port, timeout: timeout).then((socket) {
|
||||
if (socket.address.type != InternetAddressType.unix) {
|
||||
socket.setOption(SocketOption.tcpNoDelay, true);
|
||||
|
||||
@@ -36,8 +36,6 @@ class ScriptManager {
|
||||
// e.g. Add/Update/Remove:Queries、Headers、Body
|
||||
async function onRequest(context, request) {
|
||||
console.log(request.url);
|
||||
//URL queries
|
||||
//request.queries["name"] = "value";
|
||||
//Update or add Header
|
||||
//request.headers["X-New-Headers"] = "My-Value";
|
||||
|
||||
@@ -312,28 +310,48 @@ class LogInfo {
|
||||
class ScriptItem {
|
||||
bool enabled = true;
|
||||
String? name;
|
||||
String url;
|
||||
List<String> urls;
|
||||
String? scriptPath;
|
||||
RegExp? urlReg;
|
||||
List<RegExp?>? urlRegs;
|
||||
|
||||
ScriptItem(this.enabled, this.name, this.url, {this.scriptPath});
|
||||
ScriptItem(this.enabled, this.name, dynamic urls, {this.scriptPath})
|
||||
: urls = urls is String
|
||||
? (urls.contains(',') ? urls.split(',').map((e) => e.trim()).toList() : [urls])
|
||||
: (urls is List<String> ? urls : <String>[]);
|
||||
|
||||
//匹配url
|
||||
// 匹配url,任意一个规则匹配即可
|
||||
bool match(String url) {
|
||||
urlReg ??= RegExp(this.url.replaceAll("*", ".*"));
|
||||
return urlReg!.hasMatch(url);
|
||||
urlRegs ??= urls.map((u) => RegExp(u.replaceAll("*", ".*"))).toList();
|
||||
for (final reg in urlRegs!) {
|
||||
if (reg!.hasMatch(url)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
factory ScriptItem.fromJson(Map<dynamic, dynamic> json) {
|
||||
return ScriptItem(json['enabled'], json['name'], json['url'], scriptPath: json['scriptPath']);
|
||||
final urlField = json['url'];
|
||||
List<String> urls;
|
||||
if (urlField is List) {
|
||||
urls = urlField.cast<String>();
|
||||
} else if (urlField is String) {
|
||||
urls = urlField.contains(',') ? urlField.split(',').map((e) => e.trim()).toList() : [urlField];
|
||||
} else {
|
||||
urls = <String>[];
|
||||
}
|
||||
return ScriptItem(json['enabled'], json['name'], urls, scriptPath: json['scriptPath']);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'enabled': enabled, 'name': name, 'url': url, 'scriptPath': scriptPath};
|
||||
return {
|
||||
'enabled': enabled,
|
||||
'name': name,
|
||||
'url': urls.length == 1 ? urls[0] : urls,
|
||||
'scriptPath': scriptPath
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ScriptItem{enabled: $enabled, name: $name, url: $url, scriptPath: $scriptPath}';
|
||||
return 'ScriptItem{enabled: $enabled, name: $name, url: $urls, scriptPath: $scriptPath}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +87,8 @@ class HttpProxyChannelHandler extends ChannelHandler<HttpRequest> {
|
||||
|
||||
//实现抓包代理转发
|
||||
if (httpRequest.method != HttpMethod.connect) {
|
||||
log.d(
|
||||
"[${channel.id}] streamId:${httpRequest.streamId ?? ''} ${httpRequest.protocolVersion} ${httpRequest.method.name} ${httpRequest.requestUrl}");
|
||||
// log.d(
|
||||
// "[${channel.id}] streamId:${httpRequest.streamId ?? ''} ${httpRequest.protocolVersion} ${httpRequest.method.name} ${httpRequest.requestUrl}");
|
||||
if (HostFilter.filter(httpRequest.hostAndPort?.host)) {
|
||||
await remoteChannel.write(channelContext, httpRequest);
|
||||
return;
|
||||
|
||||
@@ -44,10 +44,11 @@ class ProxyHelper {
|
||||
'blacklist': HostFilter.blacklist.toJson(),
|
||||
'scripts': await ScriptManager.instance.then((script) {
|
||||
var list = script.list.map((e) async {
|
||||
return {'name': e.name, 'enabled': e.enabled, 'url': e.url, 'script': await script.getScript(e)};
|
||||
return {'name': e.name, 'enabled': e.enabled, 'url': e.urls, 'script': await script.getScript(e)};
|
||||
});
|
||||
return Future.wait(list);
|
||||
}),
|
||||
|
||||
};
|
||||
response.body = utf8.encode(json.encode(body));
|
||||
channel.writeAndClose(channelContext, response);
|
||||
|
||||
@@ -215,7 +215,7 @@ class _RequestWidgetState extends State<RequestWidget> {
|
||||
onClick: (_) async {
|
||||
var scriptManager = await ScriptManager.instance;
|
||||
var url = widget.request.domainPath;
|
||||
var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.url == url);
|
||||
var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.urls.contains(url));
|
||||
|
||||
String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem);
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -335,7 +335,7 @@ class ScriptEdit extends StatefulWidget {
|
||||
class _ScriptEditState extends State<ScriptEdit> {
|
||||
late CodeController script;
|
||||
late TextEditingController nameController;
|
||||
late TextEditingController urlController;
|
||||
late List<TextEditingController> urlControllers;
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
|
||||
@@ -344,14 +344,18 @@ class _ScriptEditState extends State<ScriptEdit> {
|
||||
super.initState();
|
||||
script = CodeController(language: javascript, text: widget.script ?? ScriptManager.template);
|
||||
nameController = TextEditingController(text: widget.scriptItem?.name ?? widget.title);
|
||||
urlController = TextEditingController(text: widget.scriptItem?.url ?? widget.url);
|
||||
final urls = widget.scriptItem?.urls ?? (widget.url != null && widget.url!.isNotEmpty ? [widget.url!] : []);
|
||||
urlControllers =
|
||||
urls.isNotEmpty ? urls.map((u) => TextEditingController(text: u)).toList() : [TextEditingController()];
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
script.dispose();
|
||||
nameController.dispose();
|
||||
urlController.dispose();
|
||||
for (final c in urlControllers) {
|
||||
c.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -361,72 +365,130 @@ class _ScriptEditState extends State<ScriptEdit> {
|
||||
bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
|
||||
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),
|
||||
titlePadding: const EdgeInsets.only(left: 15, top: 5, right: 15),
|
||||
title: Row(children: [
|
||||
Text(localizations.scriptEdit, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||||
const SizedBox(width: 10),
|
||||
Text.rich(TextSpan(
|
||||
text: localizations.useGuide,
|
||||
style: const TextStyle(color: Colors.blue, fontSize: 14),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => DesktopMultiWindow.invokeMethod(
|
||||
0,
|
||||
"launchUrl",
|
||||
isCN
|
||||
? 'https://gitee.com/wanghongenpin/proxypin/wikis/%E8%84%9A%E6%9C%AC'
|
||||
: 'https://github.com/wanghongenpin/proxypin/wiki/Script'))),
|
||||
const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))
|
||||
]),
|
||||
actionsPadding: const EdgeInsets.only(right: 10, bottom: 10),
|
||||
actions: [
|
||||
ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(localizations.cancel)),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
if (!(formKey.currentState as FormState).validate()) {
|
||||
FlutterToastr.show("${localizations.name} URL ${localizations.cannotBeEmpty}", context,
|
||||
position: FlutterToastr.top);
|
||||
return;
|
||||
}
|
||||
//新增
|
||||
if (widget.scriptItem == null) {
|
||||
var scriptItem = ScriptItem(true, nameController.text, urlController.text);
|
||||
await (await ScriptManager.instance).addScript(scriptItem, script.text);
|
||||
} else {
|
||||
widget.scriptItem?.name = nameController.text;
|
||||
widget.scriptItem?.url = urlController.text;
|
||||
widget.scriptItem?.urlReg = null;
|
||||
(await ScriptManager.instance).updateScript(widget.scriptItem!, script.text);
|
||||
}
|
||||
|
||||
_refreshScript();
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).maybePop(true);
|
||||
}
|
||||
},
|
||||
child: Text(localizations.save)),
|
||||
],
|
||||
content: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
textField("${localizations.name}:", nameController, localizations.pleaseEnter),
|
||||
const SizedBox(height: 10),
|
||||
textField("URL:", urlController, "github.com/api/*", keyboardType: TextInputType.url),
|
||||
const SizedBox(height: 10),
|
||||
Text("${localizations.script}:"),
|
||||
const SizedBox(height: 5),
|
||||
SizedBox(
|
||||
width: 850,
|
||||
height: 380,
|
||||
child: CodeTheme(
|
||||
data: CodeThemeData(styles: monokaiSublimeTheme),
|
||||
child: SingleChildScrollView(
|
||||
child: CodeField(textStyle: const TextStyle(fontSize: 13), controller: script))))
|
||||
],
|
||||
)));
|
||||
scrollable: true,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),
|
||||
titlePadding: const EdgeInsets.only(left: 15, top: 5, right: 15),
|
||||
title: Row(children: [
|
||||
Text(localizations.scriptEdit, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||||
const SizedBox(width: 10),
|
||||
Text.rich(TextSpan(
|
||||
text: localizations.useGuide,
|
||||
style: const TextStyle(color: Colors.blue, fontSize: 14),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => DesktopMultiWindow.invokeMethod(
|
||||
0,
|
||||
"launchUrl",
|
||||
isCN
|
||||
? 'https://gitee.com/wanghongenpin/proxypin/wikis/%E8%84%9A%E6%9C%AC'
|
||||
: 'https://github.com/wanghongenpin/proxypin/wiki/Script'))),
|
||||
const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))
|
||||
]),
|
||||
actionsPadding: const EdgeInsets.only(right: 10, bottom: 10),
|
||||
actions: [
|
||||
ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(localizations.cancel)),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
if (!(formKey.currentState as FormState).validate()) {
|
||||
FlutterToastr.show("${localizations.name} URL ${localizations.cannotBeEmpty}", context,
|
||||
position: FlutterToastr.top);
|
||||
return;
|
||||
}
|
||||
final urls = urlControllers.map((c) => c.text.trim()).where((u) => u.isNotEmpty).toSet().toList();
|
||||
if (urls.isEmpty) {
|
||||
FlutterToastr.show("URL ${localizations.cannotBeEmpty}", context, position: FlutterToastr.top);
|
||||
return;
|
||||
}
|
||||
if (widget.scriptItem == null) {
|
||||
var scriptItem = ScriptItem(true, nameController.text, urls);
|
||||
await (await ScriptManager.instance).addScript(scriptItem, script.text);
|
||||
} else {
|
||||
widget.scriptItem?.name = nameController.text;
|
||||
widget.scriptItem?.urls = urls;
|
||||
widget.scriptItem?.urlRegs = null;
|
||||
(await ScriptManager.instance).updateScript(widget.scriptItem!, script.text);
|
||||
}
|
||||
_refreshScript();
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).maybePop(true);
|
||||
}
|
||||
},
|
||||
child: Text(localizations.save)),
|
||||
],
|
||||
content: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
textField("${localizations.name}:", nameController, localizations.pleaseEnter),
|
||||
const SizedBox(height: 3),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text("URL(s):"),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline, size: 20),
|
||||
tooltip: localizations.add,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
urlControllers.add(TextEditingController());
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
...List.generate(
|
||||
urlControllers.length,
|
||||
(i) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: urlControllers[i],
|
||||
validator: (val) => val?.isNotEmpty == true ? null : "",
|
||||
keyboardType: TextInputType.url,
|
||||
decoration: InputDecoration(
|
||||
hintText: "github.com/api/*",
|
||||
hintStyle: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
contentPadding: const EdgeInsets.all(10),
|
||||
errorStyle: const TextStyle(height: 0, fontSize: 0),
|
||||
focusedBorder: focusedBorder(),
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (urlControllers.length > 1)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_circle_outline, color: Colors.red),
|
||||
tooltip: localizations.delete,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
urlControllers[i].dispose();
|
||||
urlControllers.removeAt(i);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"${localizations.script}:",
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
SizedBox(
|
||||
width: 850,
|
||||
height: 380,
|
||||
child: CodeTheme(
|
||||
data: CodeThemeData(styles: monokaiSublimeTheme),
|
||||
child: SingleChildScrollView(
|
||||
child: CodeField(textStyle: const TextStyle(fontSize: 13), controller: script))))
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget textField(String label, TextEditingController controller, String hint, {TextInputType? keyboardType}) {
|
||||
@@ -439,6 +501,7 @@ class _ScriptEditState extends State<ScriptEdit> {
|
||||
keyboardType: keyboardType,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
contentPadding: const EdgeInsets.all(10),
|
||||
errorStyle: const TextStyle(height: 0, fontSize: 0),
|
||||
focusedBorder: focusedBorder(),
|
||||
@@ -573,13 +636,13 @@ class _ScriptListState extends State<ScriptList> {
|
||||
_refreshScript();
|
||||
}))),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(child: Text(list[index].url, style: const TextStyle(fontSize: 13))),
|
||||
Expanded(child: Text(list[index].urls.join(', '), style: const TextStyle(fontSize: 13))),
|
||||
],
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
showGlobalMenu(Offset offset) {
|
||||
void showGlobalMenu(Offset offset) {
|
||||
showContextMenu(context, offset, items: [
|
||||
PopupMenuItem(height: 35, child: Text(localizations.newBuilt), onTap: () => showEdit()),
|
||||
PopupMenuItem(height: 35, child: Text(localizations.export), onTap: () => export(selected.toList())),
|
||||
|
||||
@@ -260,12 +260,12 @@ class _FavoriteItemState extends State<_FavoriteItem> {
|
||||
Navigator.maybePop(context);
|
||||
var scriptManager = await ScriptManager.instance;
|
||||
var url = request.domainPath;
|
||||
var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.url == url);
|
||||
var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.urls.contains(url));
|
||||
String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem);
|
||||
|
||||
var pageRoute = MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
ScriptEdit(scriptItem: scriptItem, script: script, url: scriptItem?.url ?? url));
|
||||
ScriptEdit(scriptItem: scriptItem, script: script, urls: scriptItem?.urls ?? [url]));
|
||||
if (mounted) Navigator.push(context, pageRoute);
|
||||
},
|
||||
label: localizations.script,
|
||||
|
||||
@@ -274,15 +274,15 @@ class RequestRowState extends State<RequestRow> {
|
||||
|
||||
var scriptManager = await ScriptManager.instance;
|
||||
var url = request.domainPath;
|
||||
var scriptItem = scriptManager.list.firstWhereOrNull((it) => it.url == url);
|
||||
var scriptItem = scriptManager.list.firstWhereOrNull((it) => it.urls.contains(url));
|
||||
String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem);
|
||||
|
||||
var pageRoute = MaterialPageRoute(
|
||||
builder: (context) => ScriptEdit(
|
||||
scriptItem: scriptItem,
|
||||
script: script,
|
||||
url: scriptItem?.url ?? url,
|
||||
title: request.hostAndPort?.host));
|
||||
scriptItem: scriptItem,
|
||||
script: script,
|
||||
urls: scriptItem?.urls ?? [url],
|
||||
title: request.hostAndPort?.host));
|
||||
|
||||
Navigator.push(getContext(), pageRoute);
|
||||
},
|
||||
|
||||
@@ -112,13 +112,13 @@ class _MobileScriptState extends State<MobileScript> {
|
||||
]))));
|
||||
}
|
||||
|
||||
consoleLog() {
|
||||
void consoleLog() {
|
||||
// FloatingWindowManager().show(context);
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ScriptConsoleLog()));
|
||||
}
|
||||
|
||||
//导入js
|
||||
import() async {
|
||||
Future<void> import() async {
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.any);
|
||||
if (result == null || result.files.isEmpty) {
|
||||
return;
|
||||
@@ -377,10 +377,10 @@ class _ScriptLogSmallWindowState extends State<ScriptLogSmallWindow> {
|
||||
class ScriptEdit extends StatefulWidget {
|
||||
final ScriptItem? scriptItem;
|
||||
final String? script;
|
||||
final String? url;
|
||||
final List<String>? urls;
|
||||
final String? title;
|
||||
|
||||
const ScriptEdit({super.key, this.scriptItem, this.script, this.url, this.title});
|
||||
const ScriptEdit({super.key, this.scriptItem, this.script, this.urls, this.title});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ScriptEditState();
|
||||
@@ -389,23 +389,28 @@ class ScriptEdit extends StatefulWidget {
|
||||
class _ScriptEditState extends State<ScriptEdit> {
|
||||
late CodeController script;
|
||||
late TextEditingController nameController;
|
||||
late TextEditingController urlController;
|
||||
late List<TextEditingController> urlControllers;
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final urls =
|
||||
widget.scriptItem?.urls ?? (widget.urls != null && widget.urls!.isNotEmpty ? widget.urls! : <String>[]);
|
||||
urlControllers =
|
||||
urls.isNotEmpty ? urls.map((u) => TextEditingController(text: u)).toList() : [TextEditingController()];
|
||||
script = CodeController(language: javascript, text: widget.script ?? ScriptManager.template);
|
||||
nameController = TextEditingController(text: widget.scriptItem?.name ?? widget.title);
|
||||
urlController = TextEditingController(text: widget.scriptItem?.url ?? widget.url);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final c in urlControllers) {
|
||||
c.dispose();
|
||||
}
|
||||
script.dispose();
|
||||
nameController.dispose();
|
||||
urlController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -437,15 +442,20 @@ class _ScriptEditState extends State<ScriptEdit> {
|
||||
position: FlutterToastr.top);
|
||||
return;
|
||||
}
|
||||
//新增
|
||||
// 收集所有非空、去重的 url
|
||||
final urls = urlControllers.map((c) => c.text.trim()).where((u) => u.isNotEmpty).toSet().toList();
|
||||
if (urls.isEmpty) {
|
||||
FlutterToastr.show("URL ${localizations.cannotBeEmpty}", context, position: FlutterToastr.top);
|
||||
return;
|
||||
}
|
||||
var scriptManager = await ScriptManager.instance;
|
||||
if (widget.scriptItem == null) {
|
||||
var scriptItem = ScriptItem(true, nameController.text, urlController.text);
|
||||
var scriptItem = ScriptItem(true, nameController.text, urls);
|
||||
await scriptManager.addScript(scriptItem, script.text);
|
||||
} else {
|
||||
widget.scriptItem?.name = nameController.text;
|
||||
widget.scriptItem?.url = urlController.text;
|
||||
widget.scriptItem?.urlReg = null;
|
||||
widget.scriptItem?.urls = urls;
|
||||
widget.scriptItem?.urlRegs = null;
|
||||
await scriptManager.updateScript(widget.scriptItem!, script.text);
|
||||
}
|
||||
|
||||
@@ -464,19 +474,72 @@ class _ScriptEditState extends State<ScriptEdit> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: textField("${localizations.name}:", nameController, localizations.pleaseEnter)),
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(height: 3),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: textField("URL:", urlController, "github.com/api/*", keyboardType: TextInputType.url)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text("URL(s):"),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline, size: 20),
|
||||
tooltip: localizations.add,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
urlControllers.add(TextEditingController());
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
...List.generate(
|
||||
urlControllers.length,
|
||||
(i) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: urlControllers[i],
|
||||
validator: (val) => val?.isNotEmpty == true ? null : "",
|
||||
keyboardType: TextInputType.url,
|
||||
decoration: InputDecoration(
|
||||
hintText: "github.com/api/*",
|
||||
contentPadding: const EdgeInsets.all(10),
|
||||
errorStyle: const TextStyle(height: 0, fontSize: 0),
|
||||
focusedBorder: focusedBorder(),
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (urlControllers.length > 1)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_circle_outline, color: Colors.red),
|
||||
tooltip: localizations.delete,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
urlControllers[i].dispose();
|
||||
urlControllers.removeAt(i);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(children: [
|
||||
SizedBox(width: 10),
|
||||
Text("${localizations.script}:"),
|
||||
Text(
|
||||
"${localizations.script}:",
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
IconButton(
|
||||
tooltip: localizations.copy,
|
||||
onPressed: () {
|
||||
//复制
|
||||
Clipboard.setData(ClipboardData(text: script.text));
|
||||
FlutterToastr.show(localizations.copied, context, position: FlutterToastr.top);
|
||||
},
|
||||
@@ -559,7 +622,7 @@ class _ScriptListState extends State<ScriptList> {
|
||||
]))));
|
||||
}
|
||||
|
||||
globalMenu() {
|
||||
Stack globalMenu() {
|
||||
return Stack(children: [
|
||||
Container(
|
||||
height: 50,
|
||||
@@ -644,14 +707,15 @@ class _ScriptListState extends State<ScriptList> {
|
||||
_refreshScript();
|
||||
}))),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(list[index].url.fixAutoLines(), style: const TextStyle(fontSize: 13))),
|
||||
Expanded(
|
||||
child: Text(list[index].urls.join(', ').fixAutoLines(), style: const TextStyle(fontSize: 13))),
|
||||
],
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
//点击菜单
|
||||
showMenus(int index) {
|
||||
void showMenus(int index) {
|
||||
setState(() {
|
||||
selected.add(index);
|
||||
});
|
||||
@@ -750,7 +814,7 @@ class _ScriptListState extends State<ScriptList> {
|
||||
Share.shareXFiles([file], fileNameOverrides: [fileName], sharePositionOrigin: box?.paintBounds);
|
||||
}
|
||||
|
||||
enableStatus(bool enable) {
|
||||
void enableStatus(bool enable) {
|
||||
for (var idx in selected) {
|
||||
widget.scripts[idx].enabled = enable;
|
||||
}
|
||||
@@ -758,7 +822,7 @@ class _ScriptListState extends State<ScriptList> {
|
||||
_refreshScript();
|
||||
}
|
||||
|
||||
removeScripts(List<int> indexes) async {
|
||||
Future<void> removeScripts(List<int> indexes) async {
|
||||
if (indexes.isEmpty) return;
|
||||
showConfirmDialog(context, content: localizations.confirmContent, onConfirm: () async {
|
||||
var scriptManager = await ScriptManager.instance;
|
||||
|
||||
Reference in New Issue
Block a user