mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-05-26 16:45:48 +08:00
域名过滤支持批量导出&编辑
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user