Added Hosts settings to support domain name mapping (#206)

This commit is contained in:
wanghongenpin
2024-11-11 02:51:03 +08:00
parent ba9be85d2d
commit 7adc190094
20 changed files with 628 additions and 116 deletions

View File

@@ -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;

View File

@@ -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) {

View File

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

View 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))));
}

View File

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

View File

@@ -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),

View File

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

View File

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

View File

@@ -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,

View File

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

View File

@@ -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),
)

View File

@@ -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),

View File

@@ -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),

View File

@@ -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'
: 'TipsBy 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'

View File

@@ -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),

View 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()))),
]),
])));
}
}

View File

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

View File

@@ -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),

View File

@@ -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),

View File

@@ -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),