diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index dfdd019..5afbb70 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -296,6 +296,7 @@ "other": "Other", "certHashName": "CA Hash Name", + "regExp": "RegExp", "systemCertName": "System Certificate Name", "qrCode": "QR Code", "scanQrCode": "Scan QR Code", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 31c9e34..d736c36 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -296,6 +296,7 @@ "other": "其他", "certHashName": "证书Hash名称", "systemCertName": "系统证书名称", + "regExp": "正则表达式", "qrCode": "二维码", "generateQrCode": "生成二维码", "scanQrCode": "扫描二维码", diff --git a/lib/ui/component/cert_hash.dart b/lib/ui/component/cert_hash.dart index a596f5e..cdd03f7 100644 --- a/lib/ui/component/cert_hash.dart +++ b/lib/ui/component/cert_hash.dart @@ -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 { @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 { 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 { 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 { } } - ButtonStyle get buttonStyle => - ButtonStyle( - padding: WidgetStateProperty.all(EdgeInsets.symmetric(horizontal: 15, vertical: 8)), - textStyle: WidgetStateProperty.all(TextStyle(fontSize: 14)), - shape: WidgetStateProperty.all( - 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.symmetric(horizontal: 15, vertical: 8)), + textStyle: WidgetStateProperty.all(TextStyle(fontSize: 14)), + shape: WidgetStateProperty.all( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))); } diff --git a/lib/ui/component/multi_window.dart b/lib/ui/component/multi_window.dart index 930d413..37ed370 100644 --- a/lib/ui/component/multi_window.dart +++ b/lib/ui/component/multi_window.dart @@ -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 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) { diff --git a/lib/ui/component/qr_code_page.dart b/lib/ui/component/qr_code_page.dart index 31c6705..fd3e4d6 100644 --- a/lib/ui/component/qr_code_page.dart +++ b/lib/ui/component/qr_code_page.dart @@ -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 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: [ diff --git a/lib/ui/component/regexp.dart b/lib/ui/component/regexp.dart new file mode 100644 index 0000000..c262705 --- /dev/null +++ b/lib/ui/component/regexp.dart @@ -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 createState() { + return _RegExpPageState(); + } +} + +class _RegExpPageState extends State { + 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.symmetric(horizontal: 15, vertical: 8)), + textStyle: WidgetStateProperty.all(TextStyle(fontSize: 14)), + shape: WidgetStateProperty.all( + 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 _buildHighlightedText() { + if (resultInput == null) return []; + + final spans = []; + 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; + }); + }); + } +} diff --git a/lib/ui/component/text_field.dart b/lib/ui/component/text_field.dart index d36e7a3..7dfdfee 100644 --- a/lib/ui/component/text_field.dart +++ b/lib/ui/component/text_field.dart @@ -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))); +} diff --git a/lib/ui/component/toolbox.dart b/lib/ui/component/toolbox.dart index 0d8a686..1e28b4a 100644 --- a/lib/ui/component/toolbox.dart +++ b/lib/ui/component/toolbox.dart @@ -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 { 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()) { diff --git a/lib/ui/desktop/toolbar/setting/setting.dart b/lib/ui/desktop/toolbar/setting/setting.dart index d71adde..de4418c 100644 --- a/lib/ui/desktop/toolbar/setting/setting.dart +++ b/lib/ui/desktop/toolbar/setting/setting.dart @@ -71,7 +71,8 @@ class _SettingState extends State { 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"))), ], diff --git a/lib/ui/mobile/request/domians.dart b/lib/ui/mobile/request/domians.dart index 97c10b5..193f404 100644 --- a/lib/ui/mobile/request/domians.dart +++ b/lib/ui/mobile/request/domians.dart @@ -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 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 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 with AutomaticKeepAliveClientMix widget.onRemove?.call(requests); } FlutterToastr.show(localizations.deleteSuccess, context); - Navigator.of(context).pop(); }); }), Container( @@ -303,12 +292,12 @@ class DomainListState extends State 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(); }, ), ], diff --git a/lib/ui/mobile/setting/request_rewrite.dart b/lib/ui/mobile/setting/request_rewrite.dart index 31c9317..a43566a 100644 --- a/lib/ui/mobile/setting/request_rewrite.dart +++ b/lib/ui/mobile/setting/request_rewrite.dart @@ -339,7 +339,7 @@ class _RequestRuleListState extends State { 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 { padding: const EdgeInsets.only(top: 10), child: Text(localizations.cancel, textAlign: TextAlign.center)), onPressed: () { - Navigator.of(context).pop(); + Navigator.of(ctx).pop(); }), ]); }).then((value) { diff --git a/lib/ui/mobile/setting/script.dart b/lib/ui/mobile/setting/script.dart index 4140b92..0a0e5c3 100644 --- a/lib/ui/mobile/setting/script.dart +++ b/lib/ui/mobile/setting/script.dart @@ -679,6 +679,9 @@ class _ScriptListState extends State { ], ); }).then((value) { + if (multiple) { + return; + } setState(() { selected.remove(index); }); diff --git a/pubspec.yaml b/pubspec.yaml index d013838..8e9392d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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