域名过滤支持批量导出&编辑

This commit is contained in:
wanghongenpin
2024-01-05 07:28:44 +08:00
parent 7b224b9f61
commit 8cd5b26e2e
11 changed files with 673 additions and 258 deletions

View File

@@ -1,7 +1,15 @@
import 'dart:convert';
import 'package:file_selector/file_selector.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:network_proxy/network/bin/configuration.dart';
import 'package:network_proxy/network/components/host_filter.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:network_proxy/network/util/logger.dart';
import 'package:network_proxy/ui/component/utils.dart';
import 'package:network_proxy/ui/component/widgets.dart';
/// @author wanghongen
/// 2023/10/8
@@ -38,7 +46,7 @@ class _FilterDialogState extends State<FilterDialog> {
]),
content: SizedBox(
width: 680,
height: 460,
height: 520,
child: Flex(
direction: Axis.horizontal,
children: [
@@ -87,62 +95,10 @@ class DomainFilter extends StatefulWidget {
}
class _DomainFilterState extends State<DomainFilter> {
late DomainList domainList;
bool changed = false;
AppLocalizations get localizations => AppLocalizations.of(context)!;
@override
Widget build(BuildContext context) {
domainList = DomainList(widget.hostList);
return Column(
children: [
ListTile(
title: Text(widget.title),
subtitle: Text(widget.subtitle, style: const TextStyle(fontSize: 12)),
titleAlignment: ListTileTitleAlignment.center,
),
const SizedBox(height: 10),
ValueListenableBuilder(
valueListenable: widget.hostEnableNotifier,
builder: (_, bool enable, __) {
return SwitchListTile(
title: Text(localizations.enable),
dense: true,
value: widget.hostList.enabled,
onChanged: (value) {
widget.hostList.enabled = value;
changed = true;
widget.hostEnableNotifier.value = !widget.hostEnableNotifier.value;
});
}),
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
FilledButton.icon(
icon: const Icon(Icons.add, size: 14),
onPressed: () {
add();
},
label: Text(localizations.add, style: const TextStyle(fontSize: 12))),
const SizedBox(width: 10),
TextButton.icon(
icon: const Icon(Icons.remove, size: 14),
label: Text(localizations.delete, style: const TextStyle(fontSize: 12)),
onPressed: () {
if (domainList.selected().isEmpty) {
return;
}
changed = true;
setState(() {
widget.hostList.removeIndex(domainList.selected());
});
})
]),
domainList
],
);
}
@override
void dispose() {
if (changed) {
@@ -151,96 +107,338 @@ class _DomainFilterState extends State<DomainFilter> {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ListTile(
title: Text(widget.title),
isThreeLine: true,
subtitle: Text(widget.subtitle, style: const TextStyle(fontSize: 12)),
titleAlignment: ListTileTitleAlignment.center,
),
Row(children: [
const SizedBox(width: 8),
Text(localizations.enable),
const SizedBox(width: 10),
SwitchWidget(
scale: 0.8,
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))),
const SizedBox(width: 5),
]),
DomainList(widget.hostList, onChange: () => changed = true)
],
);
}
//导入
import() async {
XTypeGroup typeGroup = const XTypeGroup(extensions: <String>['config']);
final XFile? file = await openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
if (file == null) {
return;
}
try {
List json = jsonDecode(await file.readAsString());
for (var item in json) {
widget.hostList.add(item);
}
changed = true;
if (context.mounted) {
FlutterToastr.show(localizations.importSuccess, context);
}
setState(() {});
} catch (e, t) {
logger.e('导入失败 $file', error: e, stackTrace: t);
if (context.mounted) {
FlutterToastr.show("${localizations.importFailed} $e", context);
}
}
}
void add() {
GlobalKey formKey = GlobalKey<FormState>();
String? host;
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
scrollable: true,
content: Padding(
padding: const EdgeInsets.all(8.0),
child: Form(
key: formKey,
child: Column(children: <Widget>[
TextFormField(
decoration: const InputDecoration(labelText: 'Host', hintText: '*.example.com'),
onSaved: (val) => host = val)
]))),
actions: [
FilledButton(
child: Text(localizations.add),
onPressed: () {
(formKey.currentState as FormState).save();
if (host != null && host!.isNotEmpty) {
try {
changed = true;
widget.hostList.add(host!.trim());
setState(() {});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
}
}
Navigator.of(context).pop();
}),
ElevatedButton(
child: Text(localizations.close),
onPressed: () {
Navigator.of(context).pop();
})
]);
builder: (BuildContext context) => DomainAddDialog(hostList: widget.hostList)).then((value) {
if (value != null) {
setState(() {
changed = true;
});
}
});
}
}
class DomainAddDialog extends StatelessWidget {
final HostList hostList;
final int? index;
const DomainAddDialog({super.key, required this.hostList, this.index});
@override
Widget build(BuildContext context) {
AppLocalizations localizations = AppLocalizations.of(context)!;
GlobalKey formKey = GlobalKey<FormState>();
String? host = index == null ? null : hostList.list.elementAt(index!).pattern.replaceAll(".*", "*");
return AlertDialog(
scrollable: true,
content: Padding(
padding: const EdgeInsets.all(8.0),
child: Form(
key: formKey,
child: Column(children: <Widget>[
TextFormField(
initialValue: host,
decoration: const InputDecoration(labelText: 'Host', hintText: '*.example.com'),
validator: (val) => val == null || val.trim().isEmpty ? localizations.cannotBeEmpty : null,
onChanged: (val) => host = val)
]))),
actions: [
FilledButton(
child: Text(localizations.add),
onPressed: () {
if (!(formKey.currentState as FormState).validate()) {
return;
}
try {
if (index != null) {
hostList.list[index!] = RegExp(host!.trim().replaceAll("*", ".*"));
} else {
hostList.add(host!.trim());
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
}
Navigator.of(context).pop(host);
}),
ElevatedButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop())
]);
}
}
///域名列表
class DomainList extends StatefulWidget {
final HostList hostList;
final Function onChange;
DomainList(this.hostList) : super(key: GlobalKey<_DomainListState>());
const DomainList(this.hostList, {super.key, required this.onChange});
@override
State<StatefulWidget> createState() => _DomainListState();
List<int> selected() {
var state = (key as GlobalKey<_DomainListState>).currentState;
List<int> list = [];
state?.selected.forEach((key, value) {
if (value == true) {
list.add(key);
}
});
return list;
}
}
class _DomainListState extends State<DomainList> {
late Map<int, bool> selected = {};
Map<int, bool> selected = {};
AppLocalizations get localizations => AppLocalizations.of(context)!;
bool isPress = false;
bool changed = false;
onChanged() {
changed = true;
widget.onChange.call();
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(top: 10),
height: 300,
child: SingleChildScrollView(
child: DataTable(
border: TableBorder.symmetric(outside: BorderSide(width: 1, color: Theme.of(context).highlightColor)),
columns: <DataColumn>[
DataColumn(label: Text(localizations.domain)),
],
rows: List.generate(
widget.hostList.list.length,
(index) => DataRow(
cells: [DataCell(Text(widget.hostList.list[index].pattern.replaceAll(".*", "*")))],
selected: selected[index] == true,
onSelectChanged: (value) {
setState(() {
selected[index] = value!;
});
})),
)));
return GestureDetector(
onSecondaryTapDown: (details) => showGlobalMenu(details.globalPosition),
onTapDown: (details) {
if (selected.isEmpty) {
return;
}
if (RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.metaLeft) ||
RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.control)) {
return;
}
setState(() {
selected.clear();
});
},
child: Listener(
onPointerUp: (details) => isPress = false,
onPointerDown: (details) => isPress = true,
child: Container(
padding: const EdgeInsets.only(top: 10),
height: 380,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.withOpacity(0.2)),
),
child: SingleChildScrollView(
child: Column(children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(width: 15),
const Expanded(child: Text('Host')),
],
),
const Divider(thickness: 0.5),
Column(children: rows(widget.hostList.list))
])))));
}
List<Widget> rows(List<RegExp> list) {
var primaryColor = Theme.of(context).colorScheme.primary;
return List.generate(list.length, (index) {
return InkWell(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
hoverColor: primaryColor.withOpacity(0.3),
onSecondaryTapDown: (details) => showMenus(details, index),
onDoubleTap: () => showEdit(index),
onHover: (hover) {
if (isPress && selected[index] != true) {
setState(() {
selected[index] = true;
});
}
},
onTap: () {
if (RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.metaLeft) ||
RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.control)) {
setState(() {
selected[index] = !(selected[index] ?? false);
});
return;
}
if (selected.isEmpty) {
return;
}
setState(() {
selected.clear();
});
},
child: Container(
color: selected[index] == true
? primaryColor.withOpacity(0.8)
: index.isEven
? Colors.grey.withOpacity(0.1)
: null,
height: 38,
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
children: [
const SizedBox(width: 15),
Expanded(
child: Text(list[index].pattern.replaceAll(".*", "*"), style: const TextStyle(fontSize: 14))),
],
)));
});
}
//导出
export(List<int> indexes) async {
if (indexes.isEmpty) return;
String fileName = 'host-filters.config';
String? saveLocation = (await getSaveLocation(suggestedName: fileName))?.path;
if (saveLocation == null) {
return;
}
var list = [];
for (var index in indexes) {
String rule = widget.hostList.list[index].pattern;
list.add(rule);
}
final XFile xFile = XFile.fromData(utf8.encode(jsonEncode(list)), mimeType: 'json');
await xFile.saveTo(saveLocation);
if (context.mounted) FlutterToastr.show(localizations.exportSuccess, context);
}
//删除
Future<void> remove(List<int> indexes) async {
if (indexes.isEmpty) return;
return showConfirmDialog(context, content: localizations.requestRewriteDeleteConfirm(indexes.length),
onConfirm: () async {
widget.hostList.removeIndex(indexes);
onChanged();
setState(() {
selected.clear();
});
if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);
});
}
showEdit([int? index]) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return DomainAddDialog(hostList: widget.hostList, index: index);
}).then((value) {
if (value != null) {
setState(() {
onChanged();
});
}
});
}
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.keys.toList())),
const PopupMenuDivider(),
PopupMenuItem(
height: 35,
enabled: selected.isNotEmpty,
child: Text(localizations.deleteSelect),
onTap: () => remove(selected.keys.toList())),
]);
}
//点击菜单
showMenus(TapDownDetails details, int index) {
if (selected.length > 1) {
showGlobalMenu(details.globalPosition);
return;
}
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)),
const PopupMenuDivider(),
PopupMenuItem(
height: 35,
child: Text(localizations.delete),
onTap: () {
widget.hostList.removeIndex([index]);
onChanged();
})
]).then((value) {
setState(() {
selected.remove(index);
});
});
}
}

View File

@@ -184,6 +184,7 @@ class RequestRuleList extends StatefulWidget {
class _RequestRuleListState extends State<RequestRuleList> {
Map<int, bool> selected = {};
late List<RequestRewriteRule> rules;
bool isPress = false;
AppLocalizations get localizations => AppLocalizations.of(context)!;
@@ -193,7 +194,6 @@ class _RequestRuleListState extends State<RequestRuleList> {
rules = widget.requestRewrites.rules;
}
bool isPress = false;
@override
Widget build(BuildContext context) {