desktop hosts setting (#206)

This commit is contained in:
wanghongenpin
2024-11-10 23:24:57 +08:00
parent 7551998b0e
commit ba9be85d2d
18 changed files with 630 additions and 168 deletions

View File

@@ -101,6 +101,7 @@
"matchRule": "Match Rule",
"emptyMatchAll": "Empty means match all",
"newBuilt": "New",
"newFolder": "New Folder",
"enableSelect": "Enable Select",
"disableSelect": "Disable Select",
"deleteSelect": "Delete Select",
@@ -309,5 +310,7 @@
"timestamp": "Timestamp",
"convert": "Convert",
"time": "DateTime",
"nowTimestamp": "Now timestamp"
"nowTimestamp": "Now timestamp",
"hosts": "Hosts",
"toAddress": "To Address"
}

View File

@@ -101,6 +101,7 @@
"matchRule": "匹配规则",
"emptyMatchAll": "为空表示匹配全部",
"newBuilt": "新建",
"newFolder": "新建文件夹",
"enableSelect": "启用选择",
"disableSelect": "禁用选择",
"deleteSelect": "删除选择",
@@ -308,5 +309,7 @@
"timestamp": "时间戳",
"convert": "转换",
"time": "时间",
"nowTimestamp": "当前时间戳(秒)"
"nowTimestamp": "当前时间戳(秒)",
"hosts": "Hosts 映射",
"toAddress": "映射地址"
}

View File

@@ -22,15 +22,17 @@ import 'interceptor.dart';
/// Hosts interceptor
/// @author wanghongen
class Hosts extends Interceptor {
Future<HostsManager> get hostsManager async => await HostsManager.instance;
@override
int get priority => -1000;
@override
Future<HostAndPort> preConnect(HostAndPort hostAndPort) async {
var host = hostAndPort.host;
var hostsItem = await HostsManager.instance.getHosts(host);
var hostsItem = await hostsManager.then((it) => it.getHosts(host));
if (hostsItem != null) {
return hostAndPort.copyWith(host: hostsItem.mappingAddress);
return hostAndPort.copyWith(host: hostsItem.toAddress);
}
return hostAndPort;
}

View File

@@ -35,8 +35,11 @@ class HostsManager {
HostsManager._();
/// Singleton
static HostsManager get instance {
_instance ??= HostsManager._();
static Future<HostsManager> get instance async {
if (_instance == null) {
_instance = HostsManager._();
await _instance?.load();
}
return _instance!;
}
@@ -73,8 +76,8 @@ class HostsManager {
var hostsItem = HostsItem.fromJson(element);
if (hostsItem.parent != null) {
var list = _folderMap[hostsItem.parent!] ??= [];
list.add(hostsItem);
var children = _folderMap[hostsItem.parent!] ??= [];
children.add(hostsItem);
return;
}
@@ -99,14 +102,17 @@ class HostsManager {
(await configFile).writeAsString(json);
}
List<HostsItem> getFolderList(String parent) {
return _folderMap[parent] ?? [];
}
Future<void> addHosts(HostsItem item) async {
if (item.parent == null) {
list.add(item);
} else {
var list = _folderMap[item.parent!] ??= [];
list.add(item);
var children = _folderMap[item.parent!] ??= [];
children.add(item);
}
await flushConfig();
}
Future<HostsItem?> getHosts(String host) async {
@@ -133,19 +139,39 @@ class HostsManager {
return null;
}
removeHosts(Iterable<HostsItem> items) async {
if (items.isEmpty) return;
for (var item in items) {
if (item.parent == null) {
list.remove(item);
if (item.isFolder) {
_folderMap.remove(item.id);
}
} else {
var children = _folderMap[item.parent!] ??= [];
children.remove(item);
}
}
flushConfig();
}
}
class HostsItem {
bool enabled = true;
bool isFolder = false;
final String id;
final String? parent;
final String host;
final String mappingAddress;
String? parent;
String host;
String? toAddress;
RegExp? _hostReg;
HostsItem(this.enabled, this.host, this.mappingAddress, {String? id, this.isFolder = false, this.parent})
: id = id ?? DateTime.now().millisecondsSinceEpoch.toRadixString(36) + RandomUtil.randomString(4);
HostsItem({String? id, required this.host, this.toAddress, required this.enabled, this.isFolder = false, this.parent})
: id = id ?? generateId();
static String generateId() {
return DateTime.now().millisecondsSinceEpoch.toRadixString(36) + RandomUtil.randomString(4);
}
//匹配url
bool match(String url) {
@@ -156,11 +182,11 @@ class HostsItem {
factory HostsItem.fromJson(Map<String, dynamic> json) {
return HostsItem(
id: json['id'],
host: json['host'],
toAddress: json['toAddress'],
enabled: json['enabled'],
parent: json['parent'],
isFolder: json['isFolder'] == true,
json['enabled'],
json['host'],
json['mappingAddress'],
);
}
@@ -171,7 +197,7 @@ class HostsItem {
'enabled': enabled,
'isFolder': isFolder,
'host': host,
'mappingAddress': mappingAddress,
'toAddress': toAddress,
};
}
}

View File

@@ -17,6 +17,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@@ -57,8 +58,10 @@ class _FilterDialogState extends State<FilterDialog> {
contentPadding: const EdgeInsets.only(left: 20, right: 20),
scrollable: true,
title: Row(children: [
Text(localizations.domainFilter, style: const TextStyle(fontSize: 18)),
const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))
const Expanded(child: SizedBox()),
Text(localizations.domainFilter, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
const Expanded(child: SizedBox()),
Align(alignment: Alignment.topRight, child: CloseButton())
]),
content: SizedBox(
width: 680,
@@ -266,7 +269,8 @@ class _DomainListState extends State<DomainList> {
Map<int, bool> selected = {};
AppLocalizations get localizations => AppLocalizations.of(context)!;
bool isPress = false;
bool isPressed = false;
Offset? lastPressPosition;
bool changed = false;
onChanged() {
@@ -291,8 +295,13 @@ class _DomainListState extends State<DomainList> {
});
},
child: Listener(
onPointerUp: (details) => isPress = false,
onPointerDown: (details) => isPress = true,
onPointerUp: (event) => isPressed = false,
onPointerDown: (event) {
lastPressPosition = event.localPosition;
if (event.buttons == kPrimaryMouseButton) {
isPressed = true;
}
},
child: Container(
padding: const EdgeInsets.only(top: 10),
height: 380,
@@ -323,7 +332,7 @@ class _DomainListState extends State<DomainList> {
//right click menus
onDoubleTap: () => showEdit(index),
onHover: (hover) {
if (isPress && selected[index] != true) {
if (isPressed && selected[index] != true) {
setState(() {
selected[index] = true;
});
@@ -347,7 +356,7 @@ class _DomainListState extends State<DomainList> {
color: selected[index] == true
? primaryColor.withOpacity(0.8)
: index.isEven
? Colors.grey.withOpacity(0.1)
? Colors.grey.withOpacity(0.15)
: null,
height: 38,
padding: const EdgeInsets.symmetric(vertical: 3),

View File

@@ -15,13 +15,17 @@
*/
import 'dart:convert';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/gestures.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设置
@@ -36,100 +40,306 @@ class HostsDialog extends StatefulWidget {
}
class _HostsDialogState extends State<HostsDialog> {
bool changed = false;
Set<HostsItem> selected = {};
Set<String> offstage = {};
bool isPressed = false;
Offset? lastPressPosition;
bool saving = false;
AppLocalizations get localizations => AppLocalizations.of(context)!;
@override
Widget build(BuildContext context) {
return AlertDialog(
titlePadding: const EdgeInsets.only(left: 20, top: 10, right: 15),
contentPadding: const EdgeInsets.only(left: 20, right: 20),
scrollable: true,
title: Row(children: [
Text('Hosts', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))
]),
content: SizedBox(
width: 550,
height: 500,
child: Column(children: [
Row(children: [
Text(localizations.enable),
const SizedBox(width: 10),
SwitchWidget(
scale: 0.8,
value: widget.hostsManager.enabled,
onChanged: (value) {
widget.hostsManager.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))),
const SizedBox(width: 5),
]),
const SizedBox(height: 8),
Container(
height: 430,
decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),
child: Column(children: [
const SizedBox(height: 5),
Row(children: [
SizedBox(width: 80, child: Text(localizations.enable, style: const TextStyle(fontSize: 14))),
Container(width: 15),
Expanded(child: Text(localizations.domain, style: TextStyle(fontSize: 14))),
Container(width: 18),
Expanded(child: Text('To Address', style: const TextStyle(fontSize: 14))),
]),
const Divider(thickness: 0.5),
Expanded(
child: ListView.builder(
itemCount: widget.hostsManager.list.length, itemBuilder: (_, index) => row(index)))
])),
]),
));
saveConfig() {
if (saving) return;
Future.delayed(const Duration(milliseconds: 3000), () {
saving = false;
});
}
Widget row(int index) {
var primaryColor = Theme.of(context).colorScheme.primary;
var list = widget.hostsManager.list;
@override
Widget build(BuildContext context) {
return GestureDetector(
onSecondaryTap: () {
if (lastPressPosition == null) {
return;
}
showGlobalMenu(lastPressPosition!);
},
onTapDown: (details) {
if (selected.isEmpty) {
return;
}
return InkWell(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
hoverColor: primaryColor.withOpacity(0.3),
// onSecondaryTapDown: (details) => showMenus(details, index),
// onDoubleTap: () => showEdit(index),
child: Container(
color: index.isEven ? Colors.grey.withOpacity(0.1) : null,
height: 38,
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
children: [
const SizedBox(width: 10),
Expanded(child: Text(list[index].host, style: const TextStyle(fontSize: 14))),
const SizedBox(width: 20),
SwitchWidget(
scale: 0.65,
value: list[index].enabled,
onChanged: (val) {
list[index].enabled = val;
setState(() {
changed = true;
});
}),
const SizedBox(width: 40),
SizedBox(width: 130, child: Text(list[index].mappingAddress, style: const TextStyle(fontSize: 14)))
],
)));
if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {
return;
}
setState(() {
selected.clear();
});
},
child: Listener(
onPointerUp: (event) => isPressed = false,
onPointerDown: (event) {
lastPressPosition = event.localPosition;
if (event.buttons == kPrimaryMouseButton) {
isPressed = true;
}
},
child: AlertDialog(
titlePadding: const EdgeInsets.only(left: 20, top: 10, right: 15),
contentPadding: const EdgeInsets.symmetric(horizontal: 15),
scrollable: true,
title: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
const Expanded(child: SizedBox()),
Text('Hosts', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
const Expanded(child: SizedBox()),
Align(alignment: Alignment.topRight, child: CloseButton())
]),
content: SizedBox(
width: 550,
height: 500,
child: Column(children: [
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();
}),
const Expanded(child: SizedBox()),
FilledButton.icon(
icon: const Icon(Icons.add, size: 14),
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),
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),
onPressed: import,
label: Text(localizations.import, style: const TextStyle(fontSize: 12))),
const SizedBox(width: 5),
]),
const SizedBox(height: 8),
Container(
height: 430,
decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),
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: [
InkWell(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
hoverColor: primaryColor.withOpacity(0.3),
onSecondaryTapDown: (details) => showMenus(details, item),
onDoubleTap: item.isFolder ? null : () => showEdit(item: item),
onTap: () {
if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {
setState(() {
selected.contains(item) ? selected.remove(item) : selected.add(item);
});
return;
}
if (!isPressed && selected.isNotEmpty) {
setState(() {
selected.clear();
});
return;
}
if (item.isFolder) {
setState(() {
offstage.contains(item.id) ? offstage.remove(item.id) : offstage.add(item.id);
});
}
},
onHover: (hover) {
if (isPressed && !selected.contains(item)) {
setState(() {
selected.add(item);
});
}
},
child: Container(
color: selected.contains(item)
? primaryColor.withOpacity(0.6)
: isEven
? Colors.grey.withOpacity(0.15)
: null,
height: 35,
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() {
showDialog(context: context, builder: (BuildContext context) => FolderDialog(hostsManager: widget.hostsManager))
.then((value) {
if (value != null) {
setState(() {
saveConfig();
});
}
});
}
enableStatus(bool enable) {
if (selected.isEmpty) return;
for (var item in selected) {
if (item.enabled == enable) continue;
item.enabled = enable;
}
setState(() {
saveConfig();
});
}
showGlobalMenu(Offset offset) {
showContextMenu(context, offset, items: [
PopupMenuItem(height: 35, child: Text(localizations.newBuilt), onTap: () => showEdit()),
PopupMenuItem(
height: 35, enabled: selected.isNotEmpty, child: Text(localizations.export), onTap: () => export(selected)),
const PopupMenuDivider(),
PopupMenuItem(height: 35, child: Text(localizations.enableSelect), onTap: () => enableStatus(true)),
PopupMenuItem(height: 35, child: Text(localizations.disableSelect), onTap: () => enableStatus(false)),
const PopupMenuDivider(),
PopupMenuItem(
height: 35,
enabled: selected.isNotEmpty,
child: Text(localizations.deleteSelect),
onTap: () => removeRewrite(selected)),
]);
}
//点击菜单
showMenus(TapDownDetails details, HostsItem item) {
if (selected.length > 1) {
showGlobalMenu(details.globalPosition);
return;
}
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.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);
});
});
}
showEdit({HostsItem? item, HostsItem? parent}) {
showDialog(
context: context,
builder: (BuildContext context) => item?.isFolder == true
? FolderDialog(hostsManager: widget.hostsManager, folder: item)
: HostsEditDialog(item: item, parent: parent)).then((value) {
if (value != null) {
setState(() {
saveConfig();
});
}
});
}
//删除
Future<void> removeRewrite(Set<HostsItem> items) async {
if (items.isEmpty) return;
return showConfirmDialog(context, onConfirm: () async {
await widget.hostsManager.removeHosts(items);
setState(() {
items.clear();
});
if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);
});
}
//导入
@@ -143,11 +353,23 @@ class _HostsDialogState extends State<HostsDialog> {
try {
List json = jsonDecode(await file.xFile.readAsString());
Map<String, String> idMap = {};
for (var item in json) {
// widget.hostList.add(item);
//生成新的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);
}
changed = true;
saveConfig();
if (mounted) {
FlutterToastr.show(localizations.importSuccess, context);
}
@@ -160,16 +382,184 @@ class _HostsDialogState extends State<HostsDialog> {
}
}
void add() {
// showDialog(
// context: context,
// barrierDismissible: false,
// builder: (BuildContext context) => DomainAddDialog(hostList: widget.hostList)).then((value) {
// if (value != null) {
// setState(() {
// changed = true;
// });
// }
// });
//导出
export(Iterable<HostsItem> items) async {
if (items.isEmpty) return;
String fileName = 'hosts.json';
var path = await FilePicker.platform.saveFile(fileName: fileName);
if (path == null) {
return;
}
var list = [];
for (var item in items) {
var json = item.toJson();
list.add(json);
}
await File(path).writeAsBytes(utf8.encode(jsonEncode(list)));
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(
initialValue: name,
onChanged: (val) => name = val,
decoration: InputDecoration(border: OutlineInputBorder())))
])
]),
actions: [
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)),
TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel))
],
);
}
}
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: () {
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)),
TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel))
],
content: SizedBox(
width: 300,
height: 180,
child: Form(
key: formKey,
child: Column(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

@@ -52,8 +52,10 @@ class _RequestBlockState extends State<RequestBlock> {
contentPadding: const EdgeInsets.only(left: 20, right: 20),
scrollable: true,
title: Row(children: [
Text(localizations.requestBlock, style: const TextStyle(fontSize: 16)),
const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))
const Expanded(child: SizedBox()),
Text(localizations.requestBlock, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const Expanded(child: SizedBox()),
Align(alignment: Alignment.topRight, child: CloseButton())
]),
content: SizedBox(
width: 550,
@@ -110,8 +112,8 @@ class _RequestBlockState extends State<RequestBlock> {
onSecondaryTapDown: (details) => showMenus(details, index),
onDoubleTap: () => showEdit(index),
child: Container(
color: index.isEven ? Colors.grey.withOpacity(0.1) : null,
height: 38,
color: index.isEven ? Colors.grey.withOpacity(0.15) : null,
height: 36,
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
children: [
@@ -219,7 +221,7 @@ class RequestBlockAddDialog extends StatelessWidget {
onChanged: (val) {}),
]))),
actions: [
FilledButton(
TextButton(
child: Text(localizations.save),
onPressed: () {
if (!(formKey.currentState as FormState).validate()) {
@@ -236,7 +238,7 @@ class RequestBlockAddDialog extends StatelessWidget {
}
Navigator.of(context).pop(item);
}),
ElevatedButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop())
TextButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop())
]);
}
}

View File

@@ -208,7 +208,8 @@ class RequestRuleList extends StatefulWidget {
class _RequestRuleListState extends State<RequestRuleList> {
Map<int, bool> selected = {};
late List<RequestRewriteRule> rules;
bool isPress = false;
bool isPressed = false;
Offset? lastPressPosition;
AppLocalizations get localizations => AppLocalizations.of(context)!;
@@ -221,7 +222,12 @@ class _RequestRuleListState extends State<RequestRuleList> {
@override
Widget build(BuildContext context) {
return GestureDetector(
onSecondaryTapDown: (details) => showGlobalMenu(details.globalPosition),
onSecondaryTap: () {
if (lastPressPosition == null) {
return;
}
showGlobalMenu(lastPressPosition!);
},
onTapDown: (details) {
if (selected.isEmpty) {
return;
@@ -234,8 +240,13 @@ class _RequestRuleListState extends State<RequestRuleList> {
});
},
child: Listener(
onPointerUp: (details) => isPress = false,
onPointerDown: (details) => isPress = true,
onPointerUp: (event) => isPressed = false,
onPointerDown: (event) {
lastPressPosition = event.localPosition;
if (event.buttons == kPrimaryMouseButton) {
isPressed = true;
}
},
child: Container(
padding: const EdgeInsets.only(top: 10),
constraints: const BoxConstraints(maxHeight: 600, minHeight: 550),
@@ -294,7 +305,7 @@ class _RequestRuleListState extends State<RequestRuleList> {
onSecondaryTapDown: (details) => showMenus(details, index),
onDoubleTap: () => showEdit(index),
onHover: (hover) {
if (isPress && selected[index] != true) {
if (isPressed && selected[index] != true) {
setState(() {
selected[index] = true;
});
@@ -318,7 +329,7 @@ class _RequestRuleListState extends State<RequestRuleList> {
color: selected[index] == true
? primaryColor.withOpacity(0.8)
: index.isEven
? Colors.grey.withOpacity(0.1)
? Colors.grey.withOpacity(0.15)
: null,
height: 30,
padding: const EdgeInsets.all(5),
@@ -416,9 +427,9 @@ class _RequestRuleListState extends State<RequestRuleList> {
showGlobalMenu(details.globalPosition);
return;
}
setState(() {
selected[index] = true;
});
// setState(() {
// selected[index] = true;
// });
showContextMenu(context, details.globalPosition, items: [
PopupMenuItem(height: 35, child: Text(localizations.edit), onTap: () => showEdit(index)),
PopupMenuItem(height: 35, onTap: () => export([index]), child: Text(localizations.export)),
@@ -438,9 +449,9 @@ class _RequestRuleListState extends State<RequestRuleList> {
MultiWindow.invokeRefreshRewrite(Operation.delete, index: index);
})
]).then((value) {
setState(() {
selected.remove(index);
});
// setState(() {
// selected.remove(index);
// });
});
}
}

View File

@@ -404,7 +404,7 @@ class _UpdateListState extends State<UpdateList> {
color: selected == index
? primaryColor
: index.isEven
? Colors.grey.withOpacity(0.1)
? Colors.grey.withOpacity(0.15)
: null,
height: 30,
padding: const EdgeInsets.all(5),

View File

@@ -441,7 +441,8 @@ class ScriptList extends StatefulWidget {
class _ScriptListState extends State<ScriptList> {
Set<int> selected = {};
bool isPress = false;
bool isPressed = false;
Offset? lastPressPosition;
AppLocalizations get localizations => AppLocalizations.of(context)!;
@@ -461,8 +462,13 @@ class _ScriptListState extends State<ScriptList> {
});
},
child: Listener(
onPointerUp: (details) => isPress = false,
onPointerDown: (details) => isPress = true,
onPointerUp: (event) => isPressed = false,
onPointerDown: (event) {
lastPressPosition = event.localPosition;
if (event.buttons == kPrimaryMouseButton) {
isPressed = true;
}
},
child: Container(
padding: const EdgeInsets.only(top: 10),
height: 530,
@@ -495,7 +501,7 @@ class _ScriptListState extends State<ScriptList> {
onDoubleTap: () => showEdit(index),
onSecondaryTapDown: (details) => showMenus(details, index),
onHover: (hover) {
if (isPress && !selected.contains(index)) {
if (isPressed && !selected.contains(index)) {
setState(() {
selected.add(index);
});
@@ -519,7 +525,7 @@ class _ScriptListState extends State<ScriptList> {
color: selected.contains(index)
? primaryColor.withOpacity(0.8)
: index.isEven
? Colors.grey.withOpacity(0.1)
? Colors.grey.withOpacity(0.15)
: null,
height: 30,
padding: const EdgeInsets.all(5),

View File

@@ -55,6 +55,7 @@ class _SettingState extends State<Setting> {
@override
Widget build(BuildContext context) {
return MenuAnchor(
builder: (context, controller, child) {
return IconButton(
@@ -71,7 +72,7 @@ class _SettingState extends State<Setting> {
menuChildren: [
_ProxyMenu(proxyServer: widget.proxyServer),
item(localizations.domainFilter, onPressed: hostFilter),
item('Hosts', onPressed: hosts),
item(localizations.hosts, onPressed: hosts),
item(localizations.requestBlock, onPressed: showRequestBlock),
item(localizations.requestRewrite, onPressed: requestRewrite),
item(localizations.script,

View File

@@ -247,11 +247,13 @@ class _SslState extends State<SslWidget> {
barrierDismissible: false,
builder: (BuildContext context) {
return SimpleDialog(
contentPadding: const EdgeInsets.all(16),
contentPadding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15),
title: Row(children: [
const Expanded(child: SizedBox()),
Text(isCN ? "电脑HTTPS抓包配置" : "Computer HTTPS Packet Capture Configuration",
style: const TextStyle(fontSize: 16)),
const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const Expanded(child: SizedBox()),
Align(alignment: Alignment.topRight, child: CloseButton())
]),
alignment: Alignment.center,
children: list);
@@ -265,10 +267,13 @@ class _SslState extends State<SslWidget> {
builder: (BuildContext context) {
return SimpleDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),
contentPadding: const EdgeInsets.all(16),
contentPadding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15),
title: Row(children: [
Text("iOS ${localizations.caInstallGuide}", style: const TextStyle(fontSize: 16)),
const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))
const Expanded(child: SizedBox()),
Text("iOS ${localizations.caInstallGuide}",
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const Expanded(child: SizedBox()),
Align(alignment: Alignment.topRight, child: CloseButton())
]),
alignment: Alignment.center,
children: [
@@ -315,8 +320,11 @@ class _SslState extends State<SslWidget> {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),
contentPadding: const EdgeInsets.all(5),
title: Row(children: [
Text("Android ${localizations.caInstallGuide}", style: const TextStyle(fontSize: 16)),
const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))
const Expanded(child: SizedBox()),
Text("Android ${localizations.caInstallGuide}",
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const Expanded(child: SizedBox()),
Align(alignment: Alignment.topRight, child: CloseButton())
]),
content: SizedBox(
width: 600,

View File

@@ -53,9 +53,10 @@ class _ToolbarState extends State<Toolbar> {
}
bool onKeyEvent(KeyEvent event) {
if (event.logicalKey == LogicalKeyboardKey.escape) {
if (HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.escape)) {
if (ModalRoute.of(context)?.isCurrent == false) {
Navigator.of(context).pop();
Navigator.maybePop(context);
return true;
}
}

View File

@@ -337,7 +337,7 @@ class _DomainListState extends State<DomainList> {
color: selected.contains(index)
? primaryColor.withOpacity(0.8)
: index.isEven
? Colors.grey.withOpacity(0.1)
? Colors.grey.withOpacity(0.15)
: null,
height: 38,
padding: const EdgeInsets.symmetric(vertical: 3),

View File

@@ -76,7 +76,7 @@ class _RequestBlockState extends State<MobileRequestBlock> {
onLongPress: () => showMenus(index),
onTap: () => showEdit(index),
child: Container(
color: index.isEven ? Colors.grey.withOpacity(0.1) : null,
color: index.isEven ? Colors.grey.withOpacity(0.15) : null,
height: 38,
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(

View File

@@ -263,7 +263,7 @@ class _RequestRuleListState extends State<RequestRuleList> {
color: selected.contains(index)
? primaryColor.withOpacity(0.8)
: index.isEven
? Colors.grey.withOpacity(0.1)
? Colors.grey.withOpacity(0.15)
: 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.1)
? Colors.grey.withOpacity(0.15)
: null,
constraints: const BoxConstraints(minHeight: 38, maxHeight: 45),
padding: const EdgeInsets.all(5),

View File

@@ -610,7 +610,7 @@ class _ScriptListState extends State<ScriptList> {
color: selected.contains(index)
? primaryColor.withOpacity(0.8)
: index.isEven
? Colors.grey.withOpacity(0.1)
? Colors.grey.withOpacity(0.15)
: null,
height: 45,
padding: const EdgeInsets.all(5),