Toolbox supports regular expressions (#264)

This commit is contained in:
wanghongenpin
2024-10-24 19:36:25 +08:00
parent abaf62ebbc
commit 865e1ca5e2
13 changed files with 308 additions and 97 deletions

View File

@@ -296,6 +296,7 @@
"other": "Other",
"certHashName": "CA Hash Name",
"regExp": "RegExp",
"systemCertName": "System Certificate Name",
"qrCode": "QR Code",
"scanQrCode": "Scan QR Code",

View File

@@ -296,6 +296,7 @@
"other": "其他",
"certHashName": "证书Hash名称",
"systemCertName": "系统证书名称",
"regExp": "正则表达式",
"qrCode": "二维码",
"generateQrCode": "生成二维码",
"scanQrCode": "扫描二维码",

View File

@@ -22,6 +22,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:network_proxy/network/util/cert/x509.dart';
import 'package:network_proxy/ui/component/text_field.dart';
///证书哈希名称查看
///@author Hongen Wang
@@ -50,7 +51,7 @@ class _CertHashPageState extends State<CertHashPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(localizations.systemCertName, style: TextStyle(fontSize: 16)), centerTitle: true),
appBar: AppBar(title: Text(localizations.systemCertName, style: TextStyle(fontSize: 16)), centerTitle: true),
resizeToAvoidBottomInset: false,
body: ListView(children: [
Wrap(alignment: WrapAlignment.end, children: [
@@ -93,7 +94,7 @@ class _CertHashPageState extends State<CertHashPage> {
controller: input,
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
keyboardType: TextInputType.text,
decoration: decoration(localizations.inputContent))),
decoration: decoration(context, label: localizations.inputContent))),
Align(
alignment: Alignment.bottomLeft,
child: TextButton(onPressed: () {}, child: const Text("Output:", style: TextStyle(fontSize: 16)))),
@@ -105,7 +106,7 @@ class _CertHashPageState extends State<CertHashPage> {
maxLines: 30,
readOnly: true,
controller: decodeData,
decoration: decoration('Android ${localizations.systemCertName}'))),
decoration: decoration(context, label: 'Android ${localizations.systemCertName}'))),
]));
}
@@ -124,25 +125,9 @@ class _CertHashPageState extends State<CertHashPage> {
}
}
ButtonStyle get buttonStyle =>
ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(EdgeInsets.symmetric(horizontal: 15, vertical: 8)),
textStyle: WidgetStateProperty.all<TextStyle>(TextStyle(fontSize: 14)),
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))));
InputDecoration decoration(String label, {String? hintText}) {
Color color = Theme
.of(context)
.colorScheme
.primary;
return InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: label,
hintText: hintText,
hintStyle: TextStyle(color: Colors.grey.shade500),
border: OutlineInputBorder(borderSide: BorderSide(width: 0.8, color: color)),
enabledBorder: OutlineInputBorder(borderSide: BorderSide(width: 1.5, color: color)),
focusedBorder: OutlineInputBorder(borderSide: BorderSide(width: 2, color: color)));
}
ButtonStyle get buttonStyle => ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(EdgeInsets.symmetric(horizontal: 15, vertical: 8)),
textStyle: WidgetStateProperty.all<TextStyle>(TextStyle(fontSize: 14)),
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))));
}

View File

@@ -34,6 +34,7 @@ import 'package:network_proxy/ui/component/device.dart';
import 'package:network_proxy/ui/component/encoder.dart';
import 'package:network_proxy/ui/component/js_run.dart';
import 'package:network_proxy/ui/component/qr_code_page.dart';
import 'package:network_proxy/ui/component/regexp.dart';
import 'package:network_proxy/ui/component/utils.dart';
import 'package:network_proxy/ui/content/body.dart';
import 'package:network_proxy/ui/desktop/request/request_editor.dart';
@@ -88,15 +89,17 @@ Widget multiWindow(int windowId, Map<dynamic, dynamic> argument) {
return CertHashPage();
}
//脚本日志
if (argument['name'] == 'ScriptConsoleWidget') {
return ScriptConsoleWidget(windowId: windowId);
}
if (argument['name'] == 'JavaScript') {
return const JavaScript();
}
if (argument['name'] == 'RegExpPage') {
return const RegExpPage();
}
//脚本日志
if (argument['name'] == 'ScriptConsoleWidget') {
return ScriptConsoleWidget(windowId: windowId);
}
return const SizedBox();
}
@@ -284,24 +287,6 @@ encodeWindow(EncoderType type, BuildContext context, [String? text]) async {
..show();
}
///打开脚本窗口
openScriptWindow() async {
var ratio = 1.0;
if (Platform.isWindows) {
ratio = WindowManager.instance.getDevicePixelRatio();
}
registerMethodHandler();
final window = await DesktopMultiWindow.createWindow(jsonEncode(
{'name': 'ScriptWidget'},
));
window.setTitle('Script');
window
..setFrame(const Offset(30, 0) & Size(800 * ratio, 700 * ratio))
..center()
..show();
}
openScriptConsoleWindow() async {
var ratio = 1.0;
if (Platform.isWindows) {

View File

@@ -26,6 +26,7 @@ import 'package:flutter_qr_reader/flutter_qr_reader.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:image_pickers/image_pickers.dart';
import 'package:network_proxy/ui/component/qrcode/qr_scan_view.dart';
import 'package:network_proxy/ui/component/text_field.dart';
import 'package:network_proxy/utils/platform.dart';
import 'package:qr_flutter/qr_flutter.dart';
@@ -92,18 +93,6 @@ class _QrCodePageState extends State<QrCodePage> with SingleTickerProviderStateM
}
}
InputDecoration _decoration(BuildContext context, String label, {String? hintText}) {
Color color = Theme.of(context).colorScheme.primary;
return InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: label,
hintText: hintText,
hintStyle: TextStyle(color: Colors.grey.shade500),
border: OutlineInputBorder(borderSide: BorderSide(width: 0.8, color: color)),
enabledBorder: OutlineInputBorder(borderSide: BorderSide(width: 1.3, color: color)),
focusedBorder: OutlineInputBorder(borderSide: BorderSide(width: 2, color: color)));
}
class _QrDecode extends StatefulWidget {
final int? windowId;
@@ -188,7 +177,7 @@ class _QrDecodeState extends State<_QrDecode> with AutomaticKeepAliveClientMixin
maxLines: 7,
minLines: 7,
readOnly: true,
decoration: _decoration(context, localizations.encodeResult),
decoration: decoration(context, label: localizations.encodeResult),
),
SizedBox(height: 8),
TextButton.icon(
@@ -261,7 +250,7 @@ class _QrEncodeState extends State<_QrEncode> with AutomaticKeepAliveClientMixin
controller: inputData,
maxLines: 8,
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
decoration: _decoration(context, localizations.inputContent))),
decoration: decoration(context, label: localizations.inputContent))),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [

View File

@@ -0,0 +1,232 @@
/*
* Copyright 2024 Hongen Wang All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:network_proxy/ui/component/text_field.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
///正则表达式工具
///@author Hongen Wang
class RegExpPage extends StatefulWidget {
const RegExpPage({super.key});
@override
State<StatefulWidget> createState() {
return _RegExpPageState();
}
}
class _RegExpPageState extends State<RegExpPage> {
var pattern = TextEditingController();
var input = HighlightTextEditingController();
var replaceText = TextEditingController();
String? resultInput;
AppLocalizations get localizations => AppLocalizations.of(context)!;
@override
void initState() {
super.initState();
pattern.addListener(onInputChangeMatch);
input.addListener(onInputChangeMatch);
}
@override
void dispose() {
pattern.dispose();
input.dispose();
replaceText.dispose();
super.dispose();
}
ButtonStyle get buttonStyle => ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(EdgeInsets.symmetric(horizontal: 15, vertical: 8)),
textStyle: WidgetStateProperty.all<TextStyle>(TextStyle(fontSize: 14)),
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))));
@override
Widget build(BuildContext context) {
Color primaryColor = Theme.of(context).colorScheme.primary;
return Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(40),
child: AppBar(
title: Text(localizations.regExp, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
centerTitle: true)),
resizeToAvoidBottomInset: false,
body: ListView(padding: const EdgeInsets.all(10), children: [
TextField(
controller: pattern,
minLines: 1,
maxLines: 3,
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
decoration: decoration(context,
label: 'Pattern',
hintText: 'Enter a regular expression',
suffixIcon: IconButton(icon: Icon(Icons.clear), onPressed: () => pattern.clear())),
),
const SizedBox(height: 5),
Wrap(
spacing: 8,
children: [
TextButton(
onPressed: () => pattern.text += r'\d+', // Only digits
child: const Text('Digits'),
),
TextButton(
onPressed: () => pattern.text += r'[a-zA-Z]+', // Only letters
child: const Text('Letters'),
),
TextButton(
onPressed: () => pattern.text += r'[a-zA-Z0-9]+', // Alphanumeric
child: const Text('Alphanumeric'),
),
TextButton(
onPressed: () => pattern.text += r'\w+@\w+\.\w+', // Email
child: const Text('Email'),
),
TextButton(
onPressed: () => pattern.text += r'(https?|ftp)://[^\s/$.?#].[^\s]*', // URL
child: const Text('URL'),
),
TextButton(
onPressed: () => pattern.text += r'\d{4}-\d{2}-\d{2}', // Date (YYYY-MM-DD)
child: const Text('Date (YYYY-MM-DD)'),
),
],
),
const SizedBox(height: 10),
Row(children: [
Align(alignment: Alignment.centerLeft, child: Text(localizations.testData)),
const SizedBox(width: 10),
if (!isMatch) Text(localizations.noChangesDetected, style: TextStyle(color: Colors.red))
]),
const SizedBox(height: 5),
TextField(
controller: input,
minLines: 5,
maxLines: 8,
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
decoration: decoration(context, hintText: localizations.enterMatchData),
),
const SizedBox(height: 25),
//输入替换文本
Wrap(spacing: 10, crossAxisAlignment: WrapCrossAlignment.center, children: [
SizedBox(
width: 355,
child: TextField(
controller: replaceText,
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
decoration: decoration(context, label: 'Replace Text', hintText: 'Enter replacement text'),
)),
FilledButton.icon(
onPressed: () {
if (pattern.text.isEmpty) return;
setState(() {
resultInput = input.text;
});
},
style: buttonStyle,
icon: const Icon(Icons.play_arrow_rounded),
label: const Text('Run')),
const SizedBox(width: 20),
]),
SizedBox(height: 10),
if (resultInput != null)
Row(children: [
Text("Result", style: TextStyle(fontSize: 16, color: primaryColor, fontWeight: FontWeight.w500)),
const SizedBox(width: 15),
//copy
IconButton(
icon: Icon(Icons.copy, color: primaryColor, size: 18),
onPressed: () {
Clipboard.setData(ClipboardData(text: resultInput!));
FlutterToastr.show(localizations.copied, context, duration: 3);
}),
]),
if (resultInput != null) SizedBox(height: 5),
if (resultInput != null)
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(border: Border.all(color: Theme.of(context).colorScheme.primary, width: 1.2)),
child: SelectableText.rich(
showCursor: true,
TextSpan(
children: _buildHighlightedText(),
style: Theme.of(context).textTheme.bodyLarge,
),
),
),
]));
}
List<InlineSpan> _buildHighlightedText() {
if (resultInput == null) return [];
final spans = <InlineSpan>[];
int start = 0;
var text = resultInput!;
var regex = RegExp(pattern.text);
var replaceText = this.replaceText.text;
var matches = regex.allMatches(text);
for (var match in matches) {
if (start < match.start) {
spans.add(TextSpan(text: text.substring(start, match.start)));
}
spans.add(TextSpan(text: replaceText, style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold)));
start = match.end;
}
if (start < text.length) {
spans.add(TextSpan(text: text.substring(start)));
}
return spans;
}
bool onMatch = false; //是否正在匹配
bool isMatch = true; //是否匹配成功
onInputChangeMatch() {
if (onMatch || input.highlightEnabled == false) {
return;
}
onMatch = true;
//高亮显示
Future.delayed(const Duration(milliseconds: 500), () {
onMatch = false;
if (pattern.text.isEmpty) {
if (isMatch) return;
setState(() {
isMatch = true;
});
return;
}
setState(() {
var match = input.highlight(pattern.text);
isMatch = match;
});
});
}
}

View File

@@ -60,3 +60,16 @@ class HighlightTextEditingController extends TextEditingController {
return TextSpan(children: spans, style: style);
}
}
InputDecoration decoration(BuildContext context, {String? label, String? hintText, Widget? suffixIcon}) {
Color color = Theme.of(context).colorScheme.primary;
return InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: label,
hintText: hintText,
suffixIcon: suffixIcon,
hintStyle: TextStyle(color: Colors.grey.shade500),
border: OutlineInputBorder(borderSide: BorderSide(width: 0.8, color: color)),
enabledBorder: OutlineInputBorder(borderSide: BorderSide(width: 1.3, color: color)),
focusedBorder: OutlineInputBorder(borderSide: BorderSide(width: 2, color: color)));
}

View File

@@ -10,6 +10,7 @@ import 'package:network_proxy/ui/component/encoder.dart';
import 'package:network_proxy/ui/component/js_run.dart';
import 'package:network_proxy/ui/component/multi_window.dart';
import 'package:network_proxy/ui/component/qr_code_page.dart';
import 'package:network_proxy/ui/component/regexp.dart';
import 'package:network_proxy/ui/mobile/request/request_editor.dart';
import 'package:network_proxy/utils/platform.dart';
import 'package:window_manager/window_manager.dart';
@@ -123,6 +124,17 @@ class _ToolboxState extends State<Toolbox> {
icon: Icons.key,
text: localizations.certHashName),
const SizedBox(width: 10),
IconText(
onTap: () async {
if (Platforms.isMobile()) {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const RegExpPage()));
return;
}
MultiWindow.openWindow(localizations.regExp, 'RegExpPage', size: const Size(800, 720));
},
icon: Icons.code,
text: localizations.regExp),
const SizedBox(width: 10),
IconText(
onTap: () async {
if (Platforms.isMobile()) {

View File

@@ -71,7 +71,8 @@ class _SettingState extends State<Setting> {
item(localizations.domainFilter, onPressed: hostFilter),
item(localizations.requestRewrite, onPressed: requestRewrite),
item(localizations.requestBlock, onPressed: showRequestBlock),
item(localizations.script, onPressed: () => openScriptWindow()),
item(localizations.script,
onPressed: () => MultiWindow.openWindow(localizations.script, 'ScriptWidget', size: const Size(800, 700))),
item(localizations.externalProxy, onPressed: setExternalProxy),
item("Github", onPressed: () => launchUrl(Uri.parse("https://github.com/wanghongenpin/network_proxy_flutter"))),
],

View File

@@ -27,6 +27,7 @@ import 'package:network_proxy/network/components/host_filter.dart';
import 'package:network_proxy/network/host_port.dart';
import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/network/http_client.dart';
import 'package:network_proxy/ui/component/widgets.dart';
import 'package:network_proxy/ui/mobile/request/request_sequence.dart';
import 'package:network_proxy/utils/listenable_list.dart';
@@ -226,6 +227,7 @@ class DomainListState extends State<DomainList> with AutomaticKeepAliveClientMix
///菜单
menu(int index) {
var hostAndPort = view.elementAt(index);
showModalBottomSheet(
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))),
context: context,
@@ -234,57 +236,45 @@ class DomainListState extends State<DomainList> with AutomaticKeepAliveClientMix
return Wrap(
alignment: WrapAlignment.center,
children: [
TextButton(
child: SizedBox(
width: double.infinity, child: Text(localizations.copyHost, textAlign: TextAlign.center)),
BottomSheetItem(
text: localizations.copyHost,
onPressed: () {
Clipboard.setData(ClipboardData(text: hostAndPort.host));
FlutterToastr.show(localizations.copied, context);
Navigator.of(context).pop();
}),
const Divider(thickness: 0.5),
TextButton(
child: SizedBox(
width: double.infinity, child: Text(localizations.addBlacklist, textAlign: TextAlign.center)),
const Divider(thickness: 0.5, height: 5),
BottomSheetItem(
text: localizations.addBlacklist,
onPressed: () {
HostFilter.blacklist.add(hostAndPort.host);
configuration.flushConfig();
FlutterToastr.show(localizations.addSuccess, context);
Navigator.of(context).pop();
}),
const Divider(thickness: 0.5),
TextButton(
child: SizedBox(
width: double.infinity, child: Text(localizations.addWhitelist, textAlign: TextAlign.center)),
const Divider(thickness: 0.5, height: 5),
BottomSheetItem(
text: localizations.addWhitelist,
onPressed: () {
HostFilter.whitelist.add(hostAndPort.host);
configuration.flushConfig();
FlutterToastr.show(localizations.addSuccess, context);
Navigator.of(context).pop();
}),
const Divider(thickness: 0.5),
TextButton(
child: SizedBox(
width: double.infinity, child: Text(localizations.deleteWhitelist, textAlign: TextAlign.center)),
const Divider(thickness: 0.5, height: 5),
BottomSheetItem(
text: localizations.deleteWhitelist,
onPressed: () {
HostFilter.whitelist.remove(hostAndPort.host);
configuration.flushConfig();
FlutterToastr.show(localizations.deleteSuccess, context);
Navigator.of(context).pop();
}),
const Divider(thickness: 0.5),
TextButton(
child: SizedBox(
width: double.infinity,
child: Text(localizations.repeatDomainRequests, textAlign: TextAlign.center)),
const Divider(thickness: 0.5, height: 5),
BottomSheetItem(
text: localizations.repeatDomainRequests,
onPressed: () {
Navigator.of(context).pop();
repeatDomainRequests(hostAndPort);
}),
const Divider(thickness: 0.5),
TextButton(
child:
SizedBox(width: double.infinity, child: Text(localizations.delete, textAlign: TextAlign.center)),
const Divider(thickness: 0.5, height: 5),
BottomSheetItem(
text: localizations.delete,
onPressed: () {
setState(() {
var requests = containerMap.remove(hostAndPort);
@@ -294,7 +284,6 @@ class DomainListState extends State<DomainList> with AutomaticKeepAliveClientMix
widget.onRemove?.call(requests);
}
FlutterToastr.show(localizations.deleteSuccess, context);
Navigator.of(context).pop();
});
}),
Container(
@@ -303,12 +292,12 @@ class DomainListState extends State<DomainList> with AutomaticKeepAliveClientMix
),
TextButton(
child: Container(
height: 50,
height: 45,
width: double.infinity,
padding: const EdgeInsets.only(top: 10),
child: Text(localizations.cancel, textAlign: TextAlign.center)),
onPressed: () {
Navigator.of(context).pop();
Navigator.of(ctx).pop();
},
),
],

View File

@@ -339,7 +339,7 @@ class _RequestRuleListState extends State<RequestRuleList> {
widget.requestRewrites.flushRequestRewriteConfig();
if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);
}),
Container(color: Theme.of(context).hoverColor, height: 8),
Container(color: Theme.of(ctx).hoverColor, height: 8),
TextButton(
child: Container(
height: 45,
@@ -347,7 +347,7 @@ class _RequestRuleListState extends State<RequestRuleList> {
padding: const EdgeInsets.only(top: 10),
child: Text(localizations.cancel, textAlign: TextAlign.center)),
onPressed: () {
Navigator.of(context).pop();
Navigator.of(ctx).pop();
}),
]);
}).then((value) {

View File

@@ -679,6 +679,9 @@ class _ScriptListState extends State<ScriptList> {
],
);
}).then((value) {
if (multiple) {
return;
}
setState(() {
selected.remove(index);
});

View File

@@ -30,7 +30,7 @@ dependencies:
qr_flutter: ^4.1.0
flutter_qr_reader: ^1.0.5
flutter_toastr: ^1.0.3
share_plus: ^10.1.0
share_plus: ^10.1.1
brotli: ^0.6.0
flutter_js: ^0.8.1
flutter_code_editor: ^0.3.2