mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-05-26 16:45:48 +08:00
Added Hosts settings to support domain name mapping (#206)
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
import 'package:proxypin/network/components/manager/hosts_manager.dart';
|
||||
import 'package:proxypin/network/host_port.dart';
|
||||
import 'package:proxypin/network/util/logger.dart';
|
||||
|
||||
import 'interceptor.dart';
|
||||
|
||||
@@ -32,6 +33,7 @@ class Hosts extends Interceptor {
|
||||
var host = hostAndPort.host;
|
||||
var hostsItem = await hostsManager.then((it) => it.getHosts(host));
|
||||
if (hostsItem != null) {
|
||||
logger.d('Hosts: $host -> ${hostsItem.toAddress}');
|
||||
return hostAndPort.copyWith(host: hostsItem.toAddress);
|
||||
}
|
||||
return hostAndPort;
|
||||
|
||||
@@ -174,9 +174,10 @@ class HostsItem {
|
||||
}
|
||||
|
||||
//匹配url
|
||||
bool match(String url) {
|
||||
bool match(String domain) {
|
||||
if (host != _hostReg?.pattern) _hostReg = null;
|
||||
_hostReg ??= RegExp(host.replaceAll("*", ".*"));
|
||||
return _hostReg!.hasMatch(url);
|
||||
return _hostReg!.hasMatch(domain);
|
||||
}
|
||||
|
||||
factory HostsItem.fromJson(Map<String, dynamic> json) {
|
||||
|
||||
@@ -208,7 +208,7 @@ class HttpProxyChannelHandler extends ChannelHandler<HttpRequest> {
|
||||
|
||||
HostAndPort remoteAddress = hostAndPort;
|
||||
for (var interceptor in interceptors) {
|
||||
remoteAddress = await interceptor.preConnect(hostAndPort);
|
||||
remoteAddress = await interceptor.preConnect(remoteAddress);
|
||||
}
|
||||
|
||||
final proxyChannel = await connectRemote(channelContext, clientChannel, remoteAddress);
|
||||
|
||||
8
lib/ui/component/buttons.dart
Normal file
8
lib/ui/component/buttons.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Buttons {
|
||||
static ButtonStyle get buttonStyle => ButtonStyle(
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(EdgeInsets.symmetric(horizontal: 15, vertical: 8)),
|
||||
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))));
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_toastr/flutter_toastr.dart';
|
||||
import 'package:proxypin/network/util/cert/x509.dart';
|
||||
import 'package:proxypin/ui/component/buttons.dart';
|
||||
import 'package:proxypin/ui/component/text_field.dart';
|
||||
|
||||
///证书哈希名称查看
|
||||
@@ -66,13 +67,13 @@ class _CertHashPageState extends State<CertHashPage> {
|
||||
input.text = tryDerFormat(bytes) ?? String.fromCharCodes(bytes);
|
||||
getSubjectName();
|
||||
},
|
||||
style: buttonStyle,
|
||||
style: Buttons.buttonStyle,
|
||||
icon: const Icon(Icons.folder_open),
|
||||
label: Text("File")),
|
||||
const SizedBox(width: 15),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => input.clear(),
|
||||
style: buttonStyle,
|
||||
style: Buttons.buttonStyle,
|
||||
icon: const Icon(Icons.clear),
|
||||
label: const Text("Clear")),
|
||||
const SizedBox(width: 15),
|
||||
@@ -81,7 +82,7 @@ class _CertHashPageState extends State<CertHashPage> {
|
||||
getSubjectName();
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
style: buttonStyle,
|
||||
style: Buttons.buttonStyle,
|
||||
icon: const Icon(Icons.play_arrow_rounded),
|
||||
label: const Text("Run")),
|
||||
const SizedBox(width: 15),
|
||||
@@ -139,9 +140,5 @@ class _CertHashPageState extends State<CertHashPage> {
|
||||
}
|
||||
}
|
||||
|
||||
ButtonStyle get buttonStyle => ButtonStyle(
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(EdgeInsets.symmetric(horizontal: 15, vertical: 8)),
|
||||
textStyle: WidgetStateProperty.all<TextStyle>(TextStyle(fontSize: 14)),
|
||||
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))));
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_toastr/flutter_toastr.dart';
|
||||
import 'package:proxypin/ui/component/buttons.dart';
|
||||
import 'package:proxypin/ui/component/text_field.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
@@ -54,12 +55,6 @@ class _RegExpPageState extends State<RegExpPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
ButtonStyle get buttonStyle => ButtonStyle(
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(EdgeInsets.symmetric(horizontal: 15, vertical: 8)),
|
||||
textStyle: WidgetStateProperty.all<TextStyle>(TextStyle(fontSize: 14)),
|
||||
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color primaryColor = Theme.of(context).colorScheme.primary;
|
||||
@@ -143,7 +138,7 @@ class _RegExpPageState extends State<RegExpPage> {
|
||||
resultInput = input.text;
|
||||
});
|
||||
},
|
||||
style: buttonStyle,
|
||||
style: Buttons.buttonStyle,
|
||||
icon: const Icon(Icons.play_arrow_rounded),
|
||||
label: const Text('Run')),
|
||||
const SizedBox(width: 20),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_toastr/flutter_toastr.dart';
|
||||
import 'package:proxypin/ui/component/buttons.dart';
|
||||
import 'package:proxypin/utils/lang.dart';
|
||||
import 'package:proxypin/utils/platform.dart';
|
||||
|
||||
@@ -32,12 +33,6 @@ class _TimestampPageState extends State<TimestampPage> {
|
||||
TextEditingController timestampOut = TextEditingController();
|
||||
TextEditingController dateTimeOut = TextEditingController();
|
||||
|
||||
ButtonStyle get buttonStyle => ButtonStyle(
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(EdgeInsets.symmetric(horizontal: 15, vertical: 8)),
|
||||
// textStyle: WidgetStateProperty.all<TextStyle>(TextStyle(fontSize: 14)),
|
||||
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -47,7 +42,10 @@ class _TimestampPageState extends State<TimestampPage> {
|
||||
dateTime.text = DateTime.now().format();
|
||||
//定时器
|
||||
Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
if (!mounted) timer.cancel();
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
nowTimestamp.text = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
|
||||
});
|
||||
}
|
||||
@@ -144,8 +142,8 @@ class _TimestampPageState extends State<TimestampPage> {
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: FilledButton.icon(
|
||||
icon: Icon(Icons.play_arrow_rounded),
|
||||
style: buttonStyle,
|
||||
icon: Icon(Icons.play_arrow_rounded, size: 20),
|
||||
style: Buttons.buttonStyle,
|
||||
label: Text(localizations.convert),
|
||||
onPressed: () => timestampConvert(timestamp.text)));
|
||||
}
|
||||
@@ -191,8 +189,8 @@ class _TimestampPageState extends State<TimestampPage> {
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: FilledButton.icon(
|
||||
icon: Icon(Icons.play_arrow_rounded),
|
||||
style: buttonStyle,
|
||||
icon: Icon(Icons.play_arrow_rounded, size: 20),
|
||||
style: Buttons.buttonStyle,
|
||||
label: Text(localizations.convert),
|
||||
onPressed: () => timeConvert(dateTime.text)));
|
||||
}
|
||||
|
||||
@@ -141,22 +141,17 @@ class _DomainFilterState extends State<DomainFilter> {
|
||||
Text(localizations.enable),
|
||||
const SizedBox(width: 10),
|
||||
SwitchWidget(
|
||||
scale: 0.8,
|
||||
scale: 0.75,
|
||||
value: widget.hostList.enabled,
|
||||
onChanged: (value) {
|
||||
widget.hostList.enabled = value;
|
||||
changed = true;
|
||||
}),
|
||||
const Expanded(child: SizedBox()),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.add, size: 14),
|
||||
onPressed: add,
|
||||
label: Text(localizations.add, style: const TextStyle(fontSize: 12))),
|
||||
const SizedBox(width: 10),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.input_rounded, size: 14),
|
||||
onPressed: import,
|
||||
label: Text(localizations.import, style: const TextStyle(fontSize: 12))),
|
||||
TextButton.icon(icon: const Icon(Icons.add, size: 18), onPressed: add, label: Text(localizations.add)),
|
||||
const SizedBox(width: 5),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.input_rounded, size: 18), onPressed: import, label: Text(localizations.import)),
|
||||
const SizedBox(width: 5),
|
||||
]),
|
||||
DomainList(widget.hostList, onChange: () => changed = true)
|
||||
@@ -232,8 +227,9 @@ class DomainAddDialog extends StatelessWidget {
|
||||
onChanged: (val) => host = val)
|
||||
]))),
|
||||
actions: [
|
||||
TextButton(child: Text(localizations.cancel), onPressed: () => Navigator.of(context).pop()),
|
||||
TextButton(
|
||||
child: Text(localizations.add),
|
||||
child: Text(localizations.save),
|
||||
onPressed: () {
|
||||
if (!(formKey.currentState as FormState).validate()) {
|
||||
return;
|
||||
@@ -249,7 +245,6 @@ class DomainAddDialog extends StatelessWidget {
|
||||
}
|
||||
Navigator.of(context).pop(host);
|
||||
}),
|
||||
TextButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop())
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,9 @@ class _HostsDialogState extends State<HostsDialog> {
|
||||
|
||||
saveConfig() {
|
||||
if (saving) return;
|
||||
saving = true;
|
||||
Future.delayed(const Duration(milliseconds: 3000), () {
|
||||
widget.hostsManager.flushConfig();
|
||||
saving = false;
|
||||
});
|
||||
}
|
||||
@@ -112,20 +114,20 @@ class _HostsDialogState extends State<HostsDialog> {
|
||||
saveConfig();
|
||||
}),
|
||||
const Expanded(child: SizedBox()),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.add, size: 14),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
onPressed: showEdit,
|
||||
label: Text(localizations.newBuilt, style: const TextStyle(fontSize: 12))),
|
||||
const SizedBox(width: 10),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.folder_outlined, size: 14),
|
||||
label: Text(localizations.newBuilt)),
|
||||
const SizedBox(width: 5),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.folder_outlined, size: 18),
|
||||
onPressed: newFolder,
|
||||
label: Text(localizations.newFolder, style: const TextStyle(fontSize: 12))),
|
||||
const SizedBox(width: 10),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.input_rounded, size: 14),
|
||||
label: Text(localizations.newFolder)),
|
||||
const SizedBox(width: 5),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.input_rounded, size: 18),
|
||||
onPressed: import,
|
||||
label: Text(localizations.import, style: const TextStyle(fontSize: 12))),
|
||||
label: Text(localizations.import)),
|
||||
const SizedBox(width: 5),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
@@ -236,14 +238,7 @@ class _HostsDialogState extends State<HostsDialog> {
|
||||
}
|
||||
|
||||
newFolder() {
|
||||
showDialog(context: context, builder: (BuildContext context) => FolderDialog(hostsManager: widget.hostsManager))
|
||||
.then((value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
saveConfig();
|
||||
});
|
||||
}
|
||||
});
|
||||
showEdit(isFolder: true);
|
||||
}
|
||||
|
||||
enableStatus(bool enable) {
|
||||
@@ -271,7 +266,7 @@ class _HostsDialogState extends State<HostsDialog> {
|
||||
height: 35,
|
||||
enabled: selected.isNotEmpty,
|
||||
child: Text(localizations.deleteSelect),
|
||||
onTap: () => removeRewrite(selected)),
|
||||
onTap: () => removeHosts(selected)),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -316,10 +311,11 @@ class _HostsDialogState extends State<HostsDialog> {
|
||||
});
|
||||
}
|
||||
|
||||
showEdit({HostsItem? item, HostsItem? parent}) {
|
||||
showEdit({HostsItem? item, HostsItem? parent, bool? isFolder = false}) {
|
||||
isFolder ??= item?.isFolder == true;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => item?.isFolder == true
|
||||
builder: (BuildContext context) => isFolder == true
|
||||
? FolderDialog(hostsManager: widget.hostsManager, folder: item)
|
||||
: HostsEditDialog(item: item, parent: parent)).then((value) {
|
||||
if (value != null) {
|
||||
@@ -331,7 +327,7 @@ class _HostsDialogState extends State<HostsDialog> {
|
||||
}
|
||||
|
||||
//删除
|
||||
Future<void> removeRewrite(Set<HostsItem> items) async {
|
||||
Future<void> removeHosts(Set<HostsItem> items) async {
|
||||
if (items.isEmpty) return;
|
||||
return showConfirmDialog(context, onConfirm: () async {
|
||||
await widget.hostsManager.removeHosts(items);
|
||||
@@ -433,6 +429,7 @@ class FolderDialog extends StatelessWidget {
|
||||
])
|
||||
]),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HostsItem item;
|
||||
@@ -447,7 +444,6 @@ class FolderDialog extends StatelessWidget {
|
||||
Navigator.pop(context, item);
|
||||
},
|
||||
child: Text(localizations.save)),
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel))
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -494,6 +490,7 @@ class _HostsEditDialogState extends State<HostsEditDialog> {
|
||||
return AlertDialog(
|
||||
contentPadding: const EdgeInsets.only(left: 20, right: 20, top: 10),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (!(formKey.currentState as FormState).validate()) {
|
||||
@@ -521,7 +518,6 @@ class _HostsEditDialogState extends State<HostsEditDialog> {
|
||||
Navigator.pop(context, hostItem);
|
||||
},
|
||||
child: Text(localizations.save)),
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel))
|
||||
],
|
||||
content: SizedBox(
|
||||
width: 300,
|
||||
|
||||
@@ -73,10 +73,8 @@ class _RequestBlockState extends State<RequestBlock> {
|
||||
changed = true;
|
||||
}),
|
||||
const Expanded(child: SizedBox()),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.add, size: 14),
|
||||
onPressed: showEdit,
|
||||
label: Text(localizations.add, style: const TextStyle(fontSize: 12))),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.add, size: 18), onPressed: showEdit, label: Text(localizations.add)),
|
||||
const SizedBox(width: 5),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
@@ -221,6 +219,7 @@ class RequestBlockAddDialog extends StatelessWidget {
|
||||
onChanged: (val) {}),
|
||||
]))),
|
||||
actions: [
|
||||
TextButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop()),
|
||||
TextButton(
|
||||
child: Text(localizations.save),
|
||||
onPressed: () {
|
||||
@@ -238,7 +237,6 @@ class RequestBlockAddDialog extends StatelessWidget {
|
||||
}
|
||||
Navigator.of(context).pop(item);
|
||||
}),
|
||||
TextButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop())
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,16 +124,15 @@ class RequestRewriteState extends State<RequestRewriteWidget> {
|
||||
onPressed: refresh,
|
||||
icon: const Icon(Icons.refresh, color: Colors.blue),
|
||||
tooltip: localizations.refresh),
|
||||
const SizedBox(width: 30),
|
||||
FilledButton.icon(
|
||||
const SizedBox(width: 10),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
label: Text(localizations.add, style: const TextStyle(fontSize: 12)),
|
||||
label: Text(localizations.add),
|
||||
onPressed: add,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
FilledButton.icon(
|
||||
const SizedBox(width: 5),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.input_rounded, size: 18),
|
||||
style: ElevatedButton.styleFrom(padding: const EdgeInsets.only(left: 20, right: 20)),
|
||||
onPressed: import,
|
||||
label: Text(localizations.import),
|
||||
)
|
||||
|
||||
@@ -110,7 +110,7 @@ class _ScriptWidgetState extends State<ScriptWidget> {
|
||||
subtitle: Text(localizations.scriptUseDescribe),
|
||||
trailing: SwitchWidget(
|
||||
value: data.enabled,
|
||||
scale: 0.9,
|
||||
scale: 0.8,
|
||||
onChanged: (value) {
|
||||
data.enabled = value;
|
||||
_refreshScript();
|
||||
@@ -120,18 +120,18 @@ class _ScriptWidgetState extends State<ScriptWidget> {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
FilledButton.icon(
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
onPressed: scriptAdd,
|
||||
label: Text(localizations.add)),
|
||||
const SizedBox(width: 10),
|
||||
FilledButton.icon(
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.input_rounded, size: 18),
|
||||
onPressed: import,
|
||||
label: Text(localizations.import),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
FilledButton.icon(
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.terminal, size: 18),
|
||||
onPressed: consoleLog,
|
||||
label: Text(localizations.logger),
|
||||
|
||||
@@ -17,12 +17,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:proxypin/network/bin/server.dart';
|
||||
import 'package:proxypin/network/components/manager/hosts_manager.dart';
|
||||
import 'package:proxypin/network/components/manager/request_block_manager.dart';
|
||||
import 'package:proxypin/network/components/manager/request_rewrite_manager.dart';
|
||||
import 'package:proxypin/storage/histories.dart';
|
||||
import 'package:proxypin/ui/component/utils.dart';
|
||||
import 'package:proxypin/ui/configuration.dart';
|
||||
import 'package:proxypin/ui/mobile/menu/drawer.dart';
|
||||
import 'package:proxypin/ui/mobile/setting/hosts.dart';
|
||||
import 'package:proxypin/ui/mobile/setting/preference.dart';
|
||||
import 'package:proxypin/ui/mobile/mobile.dart';
|
||||
import 'package:proxypin/ui/mobile/request/favorite.dart';
|
||||
@@ -92,6 +94,16 @@ class _MePageState extends State<MePage> {
|
||||
leading: Icon(Icons.filter_alt_outlined, color: color),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onTap: () => navigator(context, FilterMenu(proxyServer: proxyServer))),
|
||||
ListTile(
|
||||
title: Text(localizations.hosts),
|
||||
leading: Icon(Icons.domain, color: color),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onTap: () async {
|
||||
var hostsManager = await HostsManager.instance;
|
||||
if (context.mounted) {
|
||||
navigator(context, HostsPage(hostsManager: hostsManager));
|
||||
}
|
||||
}),
|
||||
ListTile(
|
||||
title: Text(localizations.requestBlock),
|
||||
leading: Icon(Icons.block_flipped, color: color),
|
||||
|
||||
@@ -259,8 +259,8 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
|
||||
|
||||
String content = isCN
|
||||
? '提示:默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n\n'
|
||||
'1. 请求重写升级UI优化, 请求修改增加匹配数据查看;\n'
|
||||
'2. 请求弹出菜单UI优化, 支持请求高亮;\n'
|
||||
'1. 新增Hosts设置, 支持域名映射;\n'
|
||||
'2. 工具箱新增时间戳转换;\n'
|
||||
'3. 脚本内置File Api, 支持文件读取、写入等操作, 详细查看wiki文档;\n'
|
||||
"4. 脚本内置MD5方法, md5('xxx');\n"
|
||||
'5. 支持内存自动清理设置, 到内存限制自动清理请求;\n'
|
||||
@@ -269,8 +269,8 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
|
||||
'8. 修复暗黑模式icon展示不清晰;\n'
|
||||
: 'Tips:By default, HTTPS packet capture will not be enabled. Please install the certificate before enabling HTTPS packet capture。\n\n'
|
||||
'Click HTTPS Capture packets(Lock icon),Choose to install the root certificate and follow the prompts to proceed。\n\n'
|
||||
'1. Request to rewrite and upgrade UI optimization, request to modify and add matching data viewing;\n'
|
||||
'2. Request pop-up menu UI optimization, support request highlighting;\n'
|
||||
'1. Added Hosts settings to support domain name mapping;\n'
|
||||
'2. Toolbox adds timestamp conversion;\n'
|
||||
'3. The script has built-in File Api, which supports file reading, writing and other operations. For details, please refer to the wiki document;\n'
|
||||
"4. The script has built-in MD5 method, md5('xxx');\n"
|
||||
'5. Support memory automatic cleanup settings, memory limit automatic cleanup requests;\n'
|
||||
|
||||
@@ -124,10 +124,10 @@ class _DomainFilterState extends State<DomainFilter> {
|
||||
});
|
||||
}),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
FilledButton.icon(icon: const Icon(Icons.add), onPressed: add, label: Text(localizations.add)),
|
||||
TextButton.icon(icon: const Icon(Icons.add, size: 20), onPressed: add, label: Text(localizations.add)),
|
||||
const SizedBox(width: 10),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.input_rounded), onPressed: import, label: Text(localizations.import)),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.input_rounded, size: 20), onPressed: import, label: Text(localizations.import)),
|
||||
const SizedBox(width: 5),
|
||||
]),
|
||||
Expanded(child: DomainList(widget.hostList, onChange: () => changed = true))
|
||||
@@ -200,7 +200,8 @@ class DomainAddDialog extends StatelessWidget {
|
||||
onChanged: (val) => host = val)
|
||||
]))),
|
||||
actions: [
|
||||
FilledButton(
|
||||
TextButton(child: Text(localizations.cancel), onPressed: () => Navigator.of(context).pop()),
|
||||
TextButton(
|
||||
child: Text(localizations.save),
|
||||
onPressed: () {
|
||||
if (!(formKey.currentState as FormState).validate()) {
|
||||
@@ -217,7 +218,6 @@ class DomainAddDialog extends StatelessWidget {
|
||||
}
|
||||
Navigator.of(context).pop(host);
|
||||
}),
|
||||
ElevatedButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop())
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -337,7 +337,7 @@ class _DomainListState extends State<DomainList> {
|
||||
color: selected.contains(index)
|
||||
? primaryColor.withOpacity(0.8)
|
||||
: index.isEven
|
||||
? Colors.grey.withOpacity(0.15)
|
||||
? Colors.grey.withOpacity(0.1)
|
||||
: null,
|
||||
height: 38,
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
|
||||
516
lib/ui/mobile/setting/hosts.dart
Normal file
516
lib/ui/mobile/setting/hosts.dart
Normal file
@@ -0,0 +1,516 @@
|
||||
/*
|
||||
* 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 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_toastr/flutter_toastr.dart';
|
||||
import 'package:proxypin/network/components/manager/hosts_manager.dart';
|
||||
import 'package:proxypin/network/util/logger.dart';
|
||||
import 'package:proxypin/ui/component/utils.dart';
|
||||
import 'package:proxypin/ui/component/widgets.dart';
|
||||
|
||||
/// Hosts page
|
||||
/// @author wanghongen
|
||||
class HostsPage extends StatefulWidget {
|
||||
final HostsManager hostsManager;
|
||||
|
||||
const HostsPage({super.key, required this.hostsManager});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _HostsPageState();
|
||||
}
|
||||
|
||||
class _HostsPageState extends State<HostsPage> {
|
||||
late HostsManager hostsManager = widget.hostsManager;
|
||||
Set<HostsItem> selected = {};
|
||||
Set<String> offstage = {};
|
||||
|
||||
bool multiple = false;
|
||||
|
||||
bool saving = false;
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
saveConfig() {
|
||||
if (saving) return;
|
||||
saving = true;
|
||||
Future.delayed(const Duration(milliseconds: 3000), () {
|
||||
widget.hostsManager.flushConfig();
|
||||
saving = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(centerTitle: true, title: Text('Hosts', style: const TextStyle(fontSize: 16))),
|
||||
persistentFooterButtons: [multiple ? globalMenu() : const SizedBox()],
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(children: [
|
||||
Container(width: 15),
|
||||
Text(localizations.enable),
|
||||
const SizedBox(width: 10),
|
||||
SwitchWidget(
|
||||
scale: 0.8,
|
||||
value: widget.hostsManager.enabled,
|
||||
onChanged: (value) {
|
||||
widget.hostsManager.enabled = value;
|
||||
saveConfig();
|
||||
}),
|
||||
]),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.add, size: 18), onPressed: showEdit, label: Text(localizations.newBuilt)),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.folder_outlined, size: 18),
|
||||
onPressed: newFolder,
|
||||
label: Text(localizations.newFolder)),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.input_rounded, size: 18),
|
||||
onPressed: import,
|
||||
label: Text(localizations.import)),
|
||||
SizedBox(width: 3),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: Column(children: [
|
||||
const SizedBox(height: 5),
|
||||
Row(children: [
|
||||
Container(width: 15),
|
||||
SizedBox(width: 50, child: Text(localizations.enable, style: const TextStyle(fontSize: 14))),
|
||||
Container(width: 15),
|
||||
Expanded(child: Text(localizations.domain, style: TextStyle(fontSize: 14))),
|
||||
Container(width: 15),
|
||||
Expanded(child: Text(localizations.toAddress, style: const TextStyle(fontSize: 14))),
|
||||
]),
|
||||
const Divider(thickness: 0.5),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.hostsManager.list.length,
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
itemBuilder: (_, index) => row(widget.hostsManager.list[index], index.isEven)))
|
||||
])),
|
||||
],
|
||||
)));
|
||||
}
|
||||
|
||||
Widget row(HostsItem item, bool isEven, {EdgeInsetsGeometry? padding}) {
|
||||
var primaryColor = Theme.of(context).colorScheme.primary;
|
||||
|
||||
return Column(children: [
|
||||
GestureDetector(
|
||||
onLongPressStart: (details) => showMenus(details, item),
|
||||
onTap: () {
|
||||
if (multiple) {
|
||||
setState(() {
|
||||
selected.contains(item) ? selected.remove(item) : selected.add(item);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.isFolder) {
|
||||
setState(() {
|
||||
offstage.contains(item.id) ? offstage.remove(item.id) : offstage.add(item.id);
|
||||
});
|
||||
return;
|
||||
}
|
||||
showEdit(item: item);
|
||||
},
|
||||
child: Container(
|
||||
color: selected.contains(item)
|
||||
? primaryColor.withOpacity(0.6)
|
||||
: isEven
|
||||
? Colors.grey.withOpacity(0.1)
|
||||
: null,
|
||||
height: 42,
|
||||
padding: padding ?? const EdgeInsets.symmetric(vertical: 3),
|
||||
child: Row(
|
||||
children: [
|
||||
SwitchWidget(
|
||||
scale: 0.6,
|
||||
value: item.enabled,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
item.enabled = val;
|
||||
saveConfig();
|
||||
});
|
||||
}),
|
||||
Container(width: 15),
|
||||
Expanded(
|
||||
child: IconText(
|
||||
icon: item.isFolder
|
||||
? Icon(offstage.contains(item.id) ? Icons.folder : Icons.folder_outlined, size: 18)
|
||||
: null,
|
||||
text: item.host,
|
||||
textStyle: const TextStyle(fontSize: 14))),
|
||||
Container(width: 15),
|
||||
Expanded(child: Text(item.toAddress ?? '', style: const TextStyle(fontSize: 14)))
|
||||
],
|
||||
))),
|
||||
if (item.isFolder)
|
||||
Offstage(
|
||||
offstage: offstage.contains(item.id),
|
||||
child: Column(
|
||||
children: widget.hostsManager
|
||||
.getFolderList(item.id)
|
||||
.map((e) => row(e, !isEven, padding: EdgeInsets.only(left: 60)))
|
||||
.toList()))
|
||||
]);
|
||||
}
|
||||
|
||||
newFolder() {
|
||||
showEdit(isFolder: true);
|
||||
}
|
||||
|
||||
showEdit({HostsItem? item, HostsItem? parent, bool? isFolder = false}) {
|
||||
isFolder ??= item?.isFolder == true;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => isFolder == true
|
||||
? FolderDialog(hostsManager: widget.hostsManager, folder: item)
|
||||
: HostsEditDialog(item: item, parent: parent)).then((value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
saveConfig();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
globalMenu() {
|
||||
return Stack(children: [
|
||||
Container(
|
||||
height: 50,
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2)))),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: TextButton(
|
||||
onPressed: () {},
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
export(selected);
|
||||
setState(() {
|
||||
selected.clear();
|
||||
multiple = false;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.share, size: 18),
|
||||
label: Text(localizations.export, style: const TextStyle(fontSize: 14))),
|
||||
TextButton.icon(
|
||||
onPressed: () => removeHosts(selected),
|
||||
icon: const Icon(Icons.delete, size: 18),
|
||||
label: Text(localizations.delete, style: const TextStyle(fontSize: 14))),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
multiple = false;
|
||||
selected.clear();
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.cancel, size: 18),
|
||||
label: Text(localizations.cancel, style: const TextStyle(fontSize: 14))),
|
||||
]))))
|
||||
]);
|
||||
}
|
||||
|
||||
//点击菜单
|
||||
showMenus(LongPressStartDetails details, HostsItem item) {
|
||||
//长按反馈
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
setState(() {
|
||||
selected.add(item);
|
||||
});
|
||||
|
||||
showContextMenu(context, details.globalPosition, items: [
|
||||
if (item.isFolder)
|
||||
PopupMenuItem(height: 35, child: Text(localizations.newBuilt), onTap: () => showEdit(parent: item)),
|
||||
PopupMenuItem(height: 35, child: Text(localizations.multiple), onTap: () => setState(() => multiple = true)),
|
||||
PopupMenuItem(height: 35, child: Text(localizations.edit), onTap: () => showEdit(item: item)),
|
||||
PopupMenuItem(height: 35, onTap: () => export([item]), child: Text(localizations.export)),
|
||||
PopupMenuItem(
|
||||
height: 35,
|
||||
child: item.enabled ? Text(localizations.disabled) : Text(localizations.enable),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
item.enabled = !item.enabled;
|
||||
saveConfig();
|
||||
});
|
||||
}),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
height: 35,
|
||||
child: Text(localizations.delete),
|
||||
onTap: () async {
|
||||
setState(() {
|
||||
widget.hostsManager.removeHosts([item]);
|
||||
});
|
||||
})
|
||||
]).then((value) {
|
||||
setState(() {
|
||||
selected.remove(item);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//删除
|
||||
Future<void> removeHosts(Set<HostsItem> items) async {
|
||||
if (items.isEmpty) return;
|
||||
return showConfirmDialog(context, onConfirm: () async {
|
||||
await widget.hostsManager.removeHosts(items);
|
||||
setState(() {
|
||||
multiple = false;
|
||||
items.clear();
|
||||
});
|
||||
if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);
|
||||
});
|
||||
}
|
||||
|
||||
//导入
|
||||
import() async {
|
||||
final FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.any);
|
||||
var file = result?.files.single;
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
List json = jsonDecode(await file.xFile.readAsString());
|
||||
Map<String, String> idMap = {};
|
||||
|
||||
for (var item in json) {
|
||||
//生成新的id 保存映射关系
|
||||
String newId = HostsItem.generateId();
|
||||
idMap[item['id']] = newId;
|
||||
item['id'] = newId;
|
||||
var hostsItem = HostsItem.fromJson(item);
|
||||
|
||||
if (hostsItem.parent != null) {
|
||||
hostsItem.parent = idMap[hostsItem.parent!];
|
||||
}
|
||||
|
||||
widget.hostsManager.addHosts(hostsItem);
|
||||
}
|
||||
|
||||
saveConfig();
|
||||
if (mounted) {
|
||||
FlutterToastr.show(localizations.importSuccess, context);
|
||||
}
|
||||
setState(() {});
|
||||
} catch (e, t) {
|
||||
logger.e('导入失败 $file', error: e, stackTrace: t);
|
||||
if (mounted) {
|
||||
FlutterToastr.show("${localizations.importFailed} $e", context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//导出
|
||||
export(Iterable<HostsItem> items) async {
|
||||
if (items.isEmpty) return;
|
||||
|
||||
String fileName = 'hosts.json';
|
||||
var list = [];
|
||||
for (var item in items) {
|
||||
var json = item.toJson();
|
||||
list.add(json);
|
||||
}
|
||||
|
||||
var path = await FilePicker.platform.saveFile(fileName: fileName, bytes: utf8.encode(jsonEncode(list)));
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
if (mounted) FlutterToastr.show(localizations.exportSuccess, context);
|
||||
}
|
||||
}
|
||||
|
||||
class FolderDialog extends StatelessWidget {
|
||||
final HostsManager hostsManager;
|
||||
final HostsItem? folder;
|
||||
|
||||
const FolderDialog({super.key, required this.hostsManager, this.folder});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
AppLocalizations localizations = AppLocalizations.of(context)!;
|
||||
bool enabled = folder?.enabled ?? true;
|
||||
String name = folder?.host ?? '';
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(localizations.newFolder, style: const TextStyle(fontSize: 16)),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Row(children: [
|
||||
SizedBox(width: 55, child: Text(localizations.enable)),
|
||||
SwitchWidget(scale: 0.8, value: enabled, onChanged: (value) => enabled = value)
|
||||
]),
|
||||
SizedBox(height: 10),
|
||||
Row(children: [
|
||||
SizedBox(width: 55, child: Text(localizations.name)),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
initialValue: name,
|
||||
onChanged: (val) => name = val,
|
||||
decoration: InputDecoration(border: OutlineInputBorder())))
|
||||
])
|
||||
]),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HostsItem item;
|
||||
if (folder == null) {
|
||||
item = HostsItem(isFolder: true, host: name, enabled: enabled);
|
||||
hostsManager.addHosts(item);
|
||||
} else {
|
||||
folder!.enabled = enabled;
|
||||
folder!.host = name;
|
||||
item = folder!;
|
||||
}
|
||||
Navigator.pop(context, item);
|
||||
},
|
||||
child: Text(localizations.save)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HostsEditDialog extends StatefulWidget {
|
||||
final HostsItem? item;
|
||||
final HostsItem? parent;
|
||||
|
||||
const HostsEditDialog({super.key, this.item, this.parent});
|
||||
|
||||
@override
|
||||
State<HostsEditDialog> createState() => _HostsEditDialogState();
|
||||
}
|
||||
|
||||
class _HostsEditDialogState extends State<HostsEditDialog> {
|
||||
GlobalKey formKey = GlobalKey<FormState>();
|
||||
|
||||
bool enabled = true;
|
||||
TextEditingController hostController = TextEditingController();
|
||||
TextEditingController toAddressController = TextEditingController();
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.item != null) {
|
||||
enabled = widget.item!.enabled;
|
||||
hostController.text = widget.item!.host;
|
||||
toAddressController.text = widget.item!.toAddress ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
hostController.dispose();
|
||||
toAddressController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
contentPadding: const EdgeInsets.only(left: 20, right: 20, top: 10),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (!(formKey.currentState as FormState).validate()) {
|
||||
FlutterToastr.show(
|
||||
"${localizations.domain} ${localizations.toAddress} ${localizations.cannotBeEmpty}", context,
|
||||
position: FlutterToastr.center);
|
||||
return;
|
||||
}
|
||||
|
||||
HostsItem? hostItem;
|
||||
if (widget.item == null) {
|
||||
hostItem = HostsItem(
|
||||
enabled: enabled,
|
||||
parent: widget.parent?.id,
|
||||
host: hostController.text,
|
||||
toAddress: toAddressController.text);
|
||||
HostsManager.instance.then((it) => it.addHosts(hostItem!));
|
||||
} else {
|
||||
widget.item!.enabled = enabled;
|
||||
widget.item!.host = hostController.text;
|
||||
widget.item!.toAddress = toAddressController.text;
|
||||
hostItem = widget.item;
|
||||
}
|
||||
|
||||
Navigator.pop(context, hostItem);
|
||||
},
|
||||
child: Text(localizations.save)),
|
||||
],
|
||||
content: Form(
|
||||
key: formKey,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Row(children: [
|
||||
SizedBox(width: 80, child: Text(localizations.enable)),
|
||||
Expanded(child: SwitchWidget(scale: 0.8, value: enabled, onChanged: (value) => enabled = value)),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
Row(children: [
|
||||
SizedBox(width: 80, child: Text(localizations.domain)),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: hostController,
|
||||
validator: (val) => val == null || val.trim().isEmpty ? localizations.cannotBeEmpty : null,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '*.example.com',
|
||||
hintStyle: TextStyle(color: Colors.grey),
|
||||
errorStyle: TextStyle(height: 0, fontSize: 0),
|
||||
border: OutlineInputBorder()))),
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
Row(children: [
|
||||
SizedBox(width: 80, child: Text(localizations.toAddress)),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: toAddressController,
|
||||
validator: (val) => val == null || val.trim().isEmpty ? localizations.cannotBeEmpty : null,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '202.108.22.5',
|
||||
errorStyle: TextStyle(height: 0, fontSize: 0),
|
||||
hintStyle: TextStyle(color: Colors.grey),
|
||||
border: OutlineInputBorder()))),
|
||||
]),
|
||||
])));
|
||||
}
|
||||
}
|
||||
@@ -36,10 +36,8 @@ class _RequestBlockState extends State<MobileRequestBlock> {
|
||||
widget.requestBlockManager.flushConfig();
|
||||
}),
|
||||
const Expanded(child: SizedBox()),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.add, size: 14),
|
||||
onPressed: showEdit,
|
||||
label: Text(localizations.add, style: const TextStyle(fontSize: 14))),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.add, size: 20), onPressed: showEdit, label: Text(localizations.add)),
|
||||
const SizedBox(width: 5),
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
@@ -76,7 +74,7 @@ class _RequestBlockState extends State<MobileRequestBlock> {
|
||||
onLongPress: () => showMenus(index),
|
||||
onTap: () => showEdit(index),
|
||||
child: Container(
|
||||
color: index.isEven ? Colors.grey.withOpacity(0.15) : null,
|
||||
color: index.isEven ? Colors.grey.withOpacity(0.1) : null,
|
||||
height: 38,
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
child: Row(
|
||||
@@ -206,7 +204,8 @@ class RequestBlockAddDialog extends StatelessWidget {
|
||||
onChanged: (val) {}),
|
||||
]))),
|
||||
actions: [
|
||||
FilledButton(
|
||||
TextButton(child: Text(localizations.cancel), onPressed: () => Navigator.of(context).pop()),
|
||||
TextButton(
|
||||
child: Text(localizations.save),
|
||||
onPressed: () {
|
||||
if (!(formKey.currentState as FormState).validate()) {
|
||||
@@ -224,7 +223,6 @@ class RequestBlockAddDialog extends StatelessWidget {
|
||||
requestBlockManager.flushConfig();
|
||||
Navigator.of(context).pop(item);
|
||||
}),
|
||||
ElevatedButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop())
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,15 +81,13 @@ class _MobileRequestRewriteState extends State<MobileRequestRewrite> {
|
||||
],
|
||||
),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.add, size: 18), onPressed: add, label: Text(localizations.add)),
|
||||
const SizedBox(width: 10),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.input_rounded, size: 18),
|
||||
style: ElevatedButton.styleFrom(padding: const EdgeInsets.only(left: 20, right: 20)),
|
||||
onPressed: import,
|
||||
label: Text(localizations.import),
|
||||
),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.add, size: 20), onPressed: add, label: Text(localizations.add)),
|
||||
const SizedBox(width: 5),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.input_rounded, size: 20),
|
||||
onPressed: import,
|
||||
label: Text(localizations.import)),
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(child: RequestRuleList(widget.requestRewrites)),
|
||||
@@ -263,7 +261,7 @@ class _RequestRuleListState extends State<RequestRuleList> {
|
||||
color: selected.contains(index)
|
||||
? primaryColor.withOpacity(0.8)
|
||||
: index.isEven
|
||||
? Colors.grey.withOpacity(0.15)
|
||||
? Colors.grey.withOpacity(0.1)
|
||||
: null,
|
||||
height: 45,
|
||||
padding: const EdgeInsets.all(5),
|
||||
|
||||
@@ -395,7 +395,7 @@ class _UpdateListState extends State<UpdateList> {
|
||||
color: selected == index
|
||||
? primaryColor
|
||||
: index.isEven
|
||||
? Colors.grey.withOpacity(0.15)
|
||||
? Colors.grey.withOpacity(0.1)
|
||||
: null,
|
||||
constraints: const BoxConstraints(minHeight: 38, maxHeight: 45),
|
||||
padding: const EdgeInsets.all(5),
|
||||
|
||||
@@ -88,19 +88,18 @@ class _MobileScriptState extends State<MobileScript> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
FilledButton.icon(
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
onPressed: scriptEdit,
|
||||
label: Text(localizations.add)),
|
||||
const SizedBox(width: 10),
|
||||
FilledButton.icon(
|
||||
const SizedBox(width: 5),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.input_rounded, size: 18),
|
||||
onPressed: import,
|
||||
label: Text(localizations.import),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
FilledButton.icon(
|
||||
const SizedBox(width: 5),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.terminal, size: 18),
|
||||
onPressed: consoleLog,
|
||||
label: Text(localizations.logger),
|
||||
@@ -610,7 +609,7 @@ class _ScriptListState extends State<ScriptList> {
|
||||
color: selected.contains(index)
|
||||
? primaryColor.withOpacity(0.8)
|
||||
: index.isEven
|
||||
? Colors.grey.withOpacity(0.15)
|
||||
? Colors.grey.withOpacity(0.1)
|
||||
: null,
|
||||
height: 45,
|
||||
padding: const EdgeInsets.all(5),
|
||||
|
||||
Reference in New Issue
Block a user