diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index eb71f07..577c75d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 212d269..fba3aad 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -101,6 +101,7 @@ "matchRule": "匹配规则", "emptyMatchAll": "为空表示匹配全部", "newBuilt": "新建", + "newFolder": "新建文件夹", "enableSelect": "启用选择", "disableSelect": "禁用选择", "deleteSelect": "删除选择", @@ -308,5 +309,7 @@ "timestamp": "时间戳", "convert": "转换", "time": "时间", - "nowTimestamp": "当前时间戳(秒)" + "nowTimestamp": "当前时间戳(秒)", + "hosts": "Hosts 映射", + "toAddress": "映射地址" } \ No newline at end of file diff --git a/lib/network/components/hosts.dart b/lib/network/components/hosts.dart index a342afd..eba68b9 100644 --- a/lib/network/components/hosts.dart +++ b/lib/network/components/hosts.dart @@ -22,15 +22,17 @@ import 'interceptor.dart'; /// Hosts interceptor /// @author wanghongen class Hosts extends Interceptor { + Future get hostsManager async => await HostsManager.instance; + @override int get priority => -1000; @override Future 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; } diff --git a/lib/network/components/manager/hosts_manager.dart b/lib/network/components/manager/hosts_manager.dart index 7aa3833..4fd981a 100644 --- a/lib/network/components/manager/hosts_manager.dart +++ b/lib/network/components/manager/hosts_manager.dart @@ -35,8 +35,11 @@ class HostsManager { HostsManager._(); /// Singleton - static HostsManager get instance { - _instance ??= HostsManager._(); + static Future 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 getFolderList(String parent) { + return _folderMap[parent] ?? []; + } + Future 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 getHosts(String host) async { @@ -133,19 +139,39 @@ class HostsManager { return null; } + + removeHosts(Iterable 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 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, }; } } diff --git a/lib/ui/desktop/toolbar/setting/filter.dart b/lib/ui/desktop/toolbar/setting/filter.dart index bae1970..bc163c8 100644 --- a/lib/ui/desktop/toolbar/setting/filter.dart +++ b/lib/ui/desktop/toolbar/setting/filter.dart @@ -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 { 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 { Map 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 { }); }, 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 { //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 { 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), diff --git a/lib/ui/desktop/toolbar/setting/hosts.dart b/lib/ui/desktop/toolbar/setting/hosts.dart index 54b48d1..811ee24 100644 --- a/lib/ui/desktop/toolbar/setting/hosts.dart +++ b/lib/ui/desktop/toolbar/setting/hosts.dart @@ -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 { - bool changed = false; + Set selected = {}; + Set 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 removeRewrite(Set 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 { try { List json = jsonDecode(await file.xFile.readAsString()); + Map 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 { } } - void add() { - // showDialog( - // context: context, - // barrierDismissible: false, - // builder: (BuildContext context) => DomainAddDialog(hostList: widget.hostList)).then((value) { - // if (value != null) { - // setState(() { - // changed = true; - // }); - // } - // }); + //导出 + export(Iterable 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 createState() => _HostsEditDialogState(); +} + +class _HostsEditDialogState extends State { + GlobalKey formKey = GlobalKey(); + + 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()))), + ]), + ])), + )); } } diff --git a/lib/ui/desktop/toolbar/setting/request_block.dart b/lib/ui/desktop/toolbar/setting/request_block.dart index 36aa768..7e6596c 100644 --- a/lib/ui/desktop/toolbar/setting/request_block.dart +++ b/lib/ui/desktop/toolbar/setting/request_block.dart @@ -52,8 +52,10 @@ class _RequestBlockState extends State { 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 { 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()) ]); } } diff --git a/lib/ui/desktop/toolbar/setting/request_rewrite.dart b/lib/ui/desktop/toolbar/setting/request_rewrite.dart index 0b239c5..52b373a 100644 --- a/lib/ui/desktop/toolbar/setting/request_rewrite.dart +++ b/lib/ui/desktop/toolbar/setting/request_rewrite.dart @@ -208,7 +208,8 @@ class RequestRuleList extends StatefulWidget { class _RequestRuleListState extends State { Map selected = {}; late List rules; - bool isPress = false; + bool isPressed = false; + Offset? lastPressPosition; AppLocalizations get localizations => AppLocalizations.of(context)!; @@ -221,7 +222,12 @@ class _RequestRuleListState extends State { @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 { }); }, 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 { 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 { 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 { 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 { MultiWindow.invokeRefreshRewrite(Operation.delete, index: index); }) ]).then((value) { - setState(() { - selected.remove(index); - }); + // setState(() { + // selected.remove(index); + // }); }); } } diff --git a/lib/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart b/lib/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart index 79b3fcf..faf08a6 100644 --- a/lib/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart +++ b/lib/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart @@ -404,7 +404,7 @@ class _UpdateListState extends State { color: selected == index ? primaryColor : index.isEven - ? Colors.grey.withOpacity(0.1) + ? Colors.grey.withOpacity(0.15) : null, height: 30, padding: const EdgeInsets.all(5), diff --git a/lib/ui/desktop/toolbar/setting/script.dart b/lib/ui/desktop/toolbar/setting/script.dart index 74d857b..9cbe273 100644 --- a/lib/ui/desktop/toolbar/setting/script.dart +++ b/lib/ui/desktop/toolbar/setting/script.dart @@ -441,7 +441,8 @@ class ScriptList extends StatefulWidget { class _ScriptListState extends State { Set selected = {}; - bool isPress = false; + bool isPressed = false; + Offset? lastPressPosition; AppLocalizations get localizations => AppLocalizations.of(context)!; @@ -461,8 +462,13 @@ class _ScriptListState extends State { }); }, 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 { 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 { 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), diff --git a/lib/ui/desktop/toolbar/setting/setting.dart b/lib/ui/desktop/toolbar/setting/setting.dart index e40c4b1..03d1f85 100644 --- a/lib/ui/desktop/toolbar/setting/setting.dart +++ b/lib/ui/desktop/toolbar/setting/setting.dart @@ -55,6 +55,7 @@ class _SettingState extends State { @override Widget build(BuildContext context) { + return MenuAnchor( builder: (context, controller, child) { return IconButton( @@ -71,7 +72,7 @@ class _SettingState extends State { 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, diff --git a/lib/ui/desktop/toolbar/ssl/ssl.dart b/lib/ui/desktop/toolbar/ssl/ssl.dart index afba2b7..fee88d9 100644 --- a/lib/ui/desktop/toolbar/ssl/ssl.dart +++ b/lib/ui/desktop/toolbar/ssl/ssl.dart @@ -247,11 +247,13 @@ class _SslState extends State { 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 { 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 { 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, diff --git a/lib/ui/desktop/toolbar/toolbar.dart b/lib/ui/desktop/toolbar/toolbar.dart index 08cd4dd..cd23fcb 100644 --- a/lib/ui/desktop/toolbar/toolbar.dart +++ b/lib/ui/desktop/toolbar/toolbar.dart @@ -53,9 +53,10 @@ class _ToolbarState extends State { } 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; } } diff --git a/lib/ui/mobile/setting/filter.dart b/lib/ui/mobile/setting/filter.dart index dde72f5..d983702 100644 --- a/lib/ui/mobile/setting/filter.dart +++ b/lib/ui/mobile/setting/filter.dart @@ -337,7 +337,7 @@ class _DomainListState extends State { 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), diff --git a/lib/ui/mobile/setting/request_block.dart b/lib/ui/mobile/setting/request_block.dart index cf37f9c..18945cf 100644 --- a/lib/ui/mobile/setting/request_block.dart +++ b/lib/ui/mobile/setting/request_block.dart @@ -76,7 +76,7 @@ class _RequestBlockState extends State { 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( diff --git a/lib/ui/mobile/setting/request_rewrite.dart b/lib/ui/mobile/setting/request_rewrite.dart index bc4960c..b99bc31 100644 --- a/lib/ui/mobile/setting/request_rewrite.dart +++ b/lib/ui/mobile/setting/request_rewrite.dart @@ -263,7 +263,7 @@ class _RequestRuleListState extends State { 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), diff --git a/lib/ui/mobile/setting/rewrite/rewrite_update.dart b/lib/ui/mobile/setting/rewrite/rewrite_update.dart index 0f81d77..bb9317b 100644 --- a/lib/ui/mobile/setting/rewrite/rewrite_update.dart +++ b/lib/ui/mobile/setting/rewrite/rewrite_update.dart @@ -395,7 +395,7 @@ class _UpdateListState extends State { 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), diff --git a/lib/ui/mobile/setting/script.dart b/lib/ui/mobile/setting/script.dart index a26c817..3bb4d39 100644 --- a/lib/ui/mobile/setting/script.dart +++ b/lib/ui/mobile/setting/script.dart @@ -610,7 +610,7 @@ class _ScriptListState extends State { 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),