new: add rich config editor

This commit is contained in:
hiddify-com
2024-07-31 03:12:35 +02:00
parent b73f83c5b5
commit 5bddd23e44
3 changed files with 436 additions and 99 deletions

View File

@@ -1 +1 @@
core.version=2.0.3 core.version=2.0.4

View File

@@ -3,7 +3,9 @@ library json_editor_flutter;
import 'dart:convert'; import 'dart:convert';
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'dart:ui';
import 'package:dartx/dartx.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -26,21 +28,265 @@ const _rightArrow = SizedBox(
child: Icon(CupertinoIcons.arrowtriangle_right_fill, size: 14), child: Icon(CupertinoIcons.arrowtriangle_right_fill, size: 14),
); );
const _newDataValue = { const _newDataValue = {
_OptionItems.string: "", "string": "",
_OptionItems.bool: false, "bool": false,
_OptionItems.num: 0, "num": 0,
}; };
bool _enableMoreOptions = true; bool _enableMoreOptions = true;
bool _enableKeyEdit = true; bool _enableKeyEdit = true;
bool _enableValueEdit = true; bool _enableValueEdit = true;
enum _OptionItems { map, list, string, bool, num, delete } // enum _OptionItems { map, list, string, bool, num, delete, protocols, configElement }
typedef _OptionItems = String;
enum _SearchActions { next, prev } enum _SearchActions { next, prev }
/// Supported editors for JSON Editor. /// Supported editors for JSON Editor.
enum Editors { tree, text } enum Editors { tree, text }
const Map<String, Map<String, dynamic>> protocolSchemaValues = {
"xray": {
"type": "xray",
"tag": "xray-out",
"xray_outbound_raw": {},
"xray_fragment": {"packets": "tlshello", "interval": "1-10", "length": "1-10"}
},
"warp": {"type": "custom", "key": "", "host": "", "port": 808, "fake_packets": "1-10", "fake_packets_size": "1-10", "fake_packets_delay": "1-10", "fake_packets_mode": "m4"},
"vless": {
"type": "vless",
"tag": "vless-out",
"server": "127.0.0.1",
"server_port": 1080,
"uuid": "bf000d23-0752-40b4-affe-68f7707a9661",
"flow": "xtls-rprx-vision",
"packet_encoding": "",
},
"vmess": {
"type": "vmess",
"tag": "vmess-out",
"server": "127.0.0.1",
"server_port": 1080,
"uuid": "bf000d23-0752-40b4-affe-68f7707a9661",
"security": "auto",
"global_padding": false,
"authenticated_length": true,
"packet_encoding": "",
},
"trojan": {
"type": "trojan",
"tag": "trojan-out",
"server": "127.0.0.1",
"server_port": 1080,
"password": "8JCsPssfgS8tiRwiMlhARg==",
},
"hysteria": {
"type": "hysteria",
"tag": "hysteria-out",
"server": "127.0.0.1",
"server_port": 1080,
"up": "100 Mbps",
"up_mbps": 100,
"down": "100 Mbps",
"down_mbps": 100,
"obfs": "daylight",
"auth": "",
"auth_str": "password",
"recv_window_conn": 0,
"recv_window": 0,
"disable_mtu_discovery": false,
"tls": {"enabled": true},
},
"hysteria2": {
"type": "hysteria2",
"tag": "hy2-out",
"server": "127.0.0.1",
"server_port": 1080,
"up_mbps": 100,
"down_mbps": 100,
"obfs": {"type": "salamander", "password": "cry_me_a_r1ver"},
"password": "goofy_ahh_password",
"tls": {
"enabled": true,
}
},
"shadowsocks": {
"type": "shadowsocks",
"tag": "ss-out",
"server": "127.0.0.1",
"server_port": 1080,
"method": "2022-blake3-aes-128-gcm",
"password": "8JCsPssfgS8tiRwiMlhARg==",
"plugin": "",
"plugin_opts": "",
"udp_over_tcp": false,
},
"socks": {
"type": "socks",
"tag": "socks-out",
"server": "127.0.0.1",
"server_port": 1080,
"version": "5",
"username": "",
"password": "",
"udp_over_tcp": false,
},
"http": {
"type": "http",
"tag": "http-out",
"server": "127.0.0.1",
"server_port": 1080,
"username": "",
"password": "",
"path": "",
"headers": {},
"tls": {"enabled": false},
},
"wireguard": {
"type": "wireguard",
"tag": "wireguard-out",
"server": "127.0.0.1",
"server_port": 1080,
"system_interface": false,
"gso": false,
"interface_name": "wg0",
"local_address": ["10.0.0.2/32"],
"private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=",
"peer_public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=",
"pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=",
"reserved": [0, 0, 0],
"workers": 4,
"mtu": 1408,
"fake_packets": "1-10",
"fake_packets_size": "1-10",
"fake_packets_delay": "1-10",
"fake_packets_mode": "m4"
},
"tuic": {
"type": "tuic",
"tag": "tuic-out",
"server": "127.0.0.1",
"server_port": 1080,
"uuid": "2DD61D93-75D8-4DA4-AC0E-6AECE7EAC365",
"password": "hello",
"congestion_control": "cubic",
"udp_relay_mode": "native",
"udp_over_stream": false,
"zero_rtt_handshake": false,
"heartbeat": "10s",
"tls": {"enabled": true},
},
"ssh": {
"type": "ssh",
"tag": "ssh-out",
"server": "127.0.0.1",
"server_port": 22,
"user": "root",
"password": "admin",
"private_key": "",
"private_key_passphrase": "",
"host_key": [""],
"client_version": "SSH-2.0-OpenSSH_7.4p1",
},
};
const Map<String, Map<String, Map<String, dynamic>>> exampleSchemaValues = {
"config.outbounds.transport": {
"browser user-agent": {
"header": {"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0"}
}
},
"config.outbounds.tls": {
"fragment": {
"tls_fragment": {"enabled": true, "size": "1-10", "sleep": "1-10"}
},
"utls": {
"utls": {"enabled": true, "fingerprint": "chrome"}
}
},
"config.outbounds": {
"multiplex": {
"multiplex": {
"enabled": true,
"protocol": "smux",
"max_connections": 4,
"min_streams": 4,
"max_streams": 0,
"padding": false,
"brutal": {"enabled": true, "up_mbps": 100, "down_mbps": 100}
},
},
"reality": {
"tls": {
"enabled": true,
"server_name": "",
"min_version": "",
"max_version": "",
"utls": {"enabled": true, "fingerprint": "chrome"},
"reality": {"enabled": true, "public_key": "", "short_id": ""}
},
},
"tls": {
"tls": {
"enabled": true,
"utls": {"enabled": true, "fingerprint": "chrome"},
"disable_sni": false,
"insecure": false,
"server_name": "",
"alpn": [],
"min_version": "",
"max_version": "",
"tls_fragment": {"enabled": false, "size": "1-10", "sleep": "1-10"}
},
},
"websocket": {
"transport": {"type": "ws", "path": "", "headers": {}, "max_early_data": 0, "early_data_header_name": ""}
},
"grpc": {
"transport": {"type": "grpc", "service_name": "TunService", "idle_timeout": "15s", "ping_timeout": "15s", "permit_without_stream": false}
},
"quic": {
"transport": {"type": "quic"}
},
"http": {
"transport": {"type": "http", "host": [], "path": "", "method": "", "headers": {}, "idle_timeout": "15s", "ping_timeout": "15s"}
},
"httpupgrade": {
"transport": {"type": "httpupgrade", "host": "", "path": "", "headers": {}}
},
}
};
const Map<String, List<String>> possibleValues = {
"config.outbounds.flow": <String>["", "xtls-rprx-vision"],
"config.outbounds.security": <String>["", "auto", "none", "zero", "aes-128-gcm", "chacha20-poly1305"],
"config.outbounds.method": <String>["", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305", "none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305"],
"config.outbounds.plugin": <String>["", "obfs-local", "v2ray-plugin"],
"config.outbounds.network": <String>["", "udp", "tcp"],
"config.outbounds.multiplex.protocol": <String>["", "smux", "yamux", "h2mux"],
"config.outbounds.tls.min_version": <String>["", "1.0", "1.1", "1.2", "1.3"],
"config.outbounds.tls.max_version": <String>["", "1.0", "1.1", "1.2", "1.3"],
"config.outbounds.tls.utls.fingerprint": <String>["", "chrome", "chrome_psk", "chrome_psk_shuffle", "chrome_padding_psk_shuffle", "chrome_pq", "chrome_pq_psk", "firefox", "edge", "safari", "360", "qq", "ios", "android", "random", "randomized"],
"config.outbounds.packet_encoding": <String>["", "(none)", "xudp", "packetaddr"],
"config.outbounds.transport.type": <String>["", "http", "ws", "grpc", "quic", "httpupgrade"],
"config.outbounds.type": <String>[
"vless",
"vmess",
"trojan",
"xray",
"shadowsocks",
"wireguard",
"hysteria",
"hysteria2",
"tuic",
"ssh",
"shadowtls",
"custom",
"direct",
"block",
"socks",
"http",
]
};
/// Edit your JSON object with this Widget. Create, edit and format objects /// Edit your JSON object with this Widget. Create, edit and format objects
/// using this user friendly widget. /// using this user friendly widget.
class JsonEditor extends StatefulWidget { class JsonEditor extends StatefulWidget {
@@ -599,6 +845,16 @@ class _Holder extends StatefulWidget {
final Map<String, bool> matchedKeys; final Map<String, bool> matchedKeys;
final List allParents; final List allParents;
final Map<String, bool> expandedObjects; final Map<String, bool> expandedObjects;
String getKeyPath() {
final basePath = allParents.whereType<String>().join('.');
// final typePath = (parentObject['type'] != null &&
// parentObject['type'] != keyName)
// ? '.${parentObject['type']}'
// : '';
return '$basePath';
}
@override @override
State<_Holder> createState() => _HolderState(); State<_Holder> createState() => _HolderState();
@@ -619,7 +875,7 @@ class _HolderState extends State<_Holder> {
} }
void onSelected(_OptionItems selectedItem) { void onSelected(_OptionItems selectedItem) {
if (selectedItem == _OptionItems.delete) { if (selectedItem == "delete") {
if (widget.parentObject is Map) { if (widget.parentObject is Map) {
widget.parentObject.remove(widget.keyName); widget.parentObject.remove(widget.keyName);
} else { } else {
@@ -627,7 +883,7 @@ class _HolderState extends State<_Holder> {
} }
widget.setState(() {}); widget.setState(() {});
} else if (selectedItem == _OptionItems.map) { } else if (selectedItem == "map") {
if (widget.data is Map) { if (widget.data is Map) {
widget.data[_newKey] = Map<String, dynamic>(); widget.data[_newKey] = Map<String, dynamic>();
} else { } else {
@@ -635,7 +891,19 @@ class _HolderState extends State<_Holder> {
} }
setState(() {}); setState(() {});
} else if (selectedItem == _OptionItems.list) { widget.onChanged();
} else if (exampleSchemaValues.containsKey(selectedItem.split("___")[0])) {
final jsonItem = exampleSchemaValues[selectedItem.split("___")[0]]![selectedItem.split("___")[1]]!;
for (final key in jsonItem.keys) {
widget.data[key] = jsonDecode(jsonEncode(jsonItem[key]));
}
setState(() {});
} else if (protocolSchemaValues.containsKey(selectedItem)) {
final jsonItem = protocolSchemaValues[selectedItem]!;
widget.data.add(jsonDecode(jsonEncode(jsonItem)));
setState(() {});
} else if (selectedItem == "list") {
if (widget.data is Map) { if (widget.data is Map) {
widget.data[_newKey] = []; widget.data[_newKey] = [];
} else { } else {
@@ -731,7 +999,7 @@ class _HolderState extends State<_Holder> {
child: Row( child: Row(
children: [ children: [
const SizedBox(width: _expandIconWidth), const SizedBox(width: _expandIconWidth),
if (_enableMoreOptions) _Options<Map>(onSelected), if (_enableMoreOptions) _Options<Map>(onSelected, widget.getKeyPath()),
SizedBox(width: widget.paddingLeft), SizedBox(width: widget.paddingLeft),
InkWell( InkWell(
hoverColor: Colors.transparent, hoverColor: Colors.transparent,
@@ -810,7 +1078,7 @@ class _HolderState extends State<_Holder> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const SizedBox(width: _expandIconWidth), const SizedBox(width: _expandIconWidth),
if (_enableMoreOptions) _Options<List>(onSelected), if (_enableMoreOptions) _Options<List>(onSelected, widget.getKeyPath()),
SizedBox(width: widget.paddingLeft), SizedBox(width: widget.paddingLeft),
InkWell( InkWell(
hoverColor: Colors.transparent, hoverColor: Colors.transparent,
@@ -868,7 +1136,7 @@ class _HolderState extends State<_Holder> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const SizedBox(width: _expandIconWidth), const SizedBox(width: _expandIconWidth),
if (_enableMoreOptions) _Options<String>(onSelected), if (_enableMoreOptions) _Options<String>(onSelected, widget.getKeyPath()),
SizedBox( SizedBox(
width: widget.paddingLeft + (_expandIconWidth * 2), width: widget.paddingLeft + (_expandIconWidth * 2),
), ),
@@ -902,6 +1170,7 @@ class _HolderState extends State<_Holder> {
_ReplaceTextWithField( _ReplaceTextWithField(
key: UniqueKey(), key: UniqueKey(),
initialValue: widget.data, initialValue: widget.data,
keyPath: widget.getKeyPath(),
onChanged: onValueChanged, onChanged: onValueChanged,
setState: setState, setState: setState,
), ),
@@ -920,15 +1189,8 @@ class _HolderState extends State<_Holder> {
} }
class _ReplaceTextWithField extends StatefulWidget { class _ReplaceTextWithField extends StatefulWidget {
const _ReplaceTextWithField({ const _ReplaceTextWithField({super.key, required this.initialValue, required this.onChanged, required this.setState, this.isKey = false, this.isHighlighted = false, this.keyPath = ""});
super.key, final String keyPath;
required this.initialValue,
required this.onChanged,
required this.setState,
this.isKey = false,
this.isHighlighted = false,
});
final dynamic initialValue; final dynamic initialValue;
final bool isKey; final bool isKey;
final ValueChanged<Object> onChanged; final ValueChanged<Object> onChanged;
@@ -1008,7 +1270,40 @@ class _ReplaceTextWithFieldState extends State<_ReplaceTextWithField> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.initialValue is bool) { if (possibleValues.containsKey(widget.keyPath)) {
final options = possibleValues[widget.keyPath]!;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Transform.scale(
scale: 0.75,
child: DropdownButton<String>(
hint: Text('Select ${widget.keyPath.replaceAll("config.outbounds", "")}'),
value: _text,
icon: Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
underline: Container(
height: 2,
),
onChanged: (String? newValue) {
widget.onChanged(newValue!);
_text = newValue;
setState(() {
_text = newValue;
});
},
items: options.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
),
],
);
} else if (widget.initialValue is bool) {
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -1068,8 +1363,8 @@ class _ReplaceTextWithFieldState extends State<_ReplaceTextWithField> {
} }
class _Options<T> extends StatelessWidget { class _Options<T> extends StatelessWidget {
const _Options(this.onSelected); const _Options(this.onSelected, this.keyPath);
final String keyPath;
final void Function(_OptionItems) onSelected; final void Function(_OptionItems) onSelected;
@override @override
@@ -1080,7 +1375,7 @@ class _Options<T> extends StatelessWidget {
onSelected: onSelected, onSelected: onSelected,
itemBuilder: (context) { itemBuilder: (context) {
return <PopupMenuEntry<_OptionItems>>[ return <PopupMenuEntry<_OptionItems>>[
if (T == Map) if (keyPath != "config" && T == Map)
const _PopupMenuWidget(Row( const _PopupMenuWidget(Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -1090,7 +1385,7 @@ class _Options<T> extends StatelessWidget {
Text("Insert", style: TextStyle(fontSize: 14)), Text("Insert", style: TextStyle(fontSize: 14)),
], ],
)), )),
if (T == List) if (keyPath != "config" && T == List)
const _PopupMenuWidget(Row( const _PopupMenuWidget(Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -1100,87 +1395,129 @@ class _Options<T> extends StatelessWidget {
Text("Append", style: TextStyle(fontSize: 14)), Text("Append", style: TextStyle(fontSize: 14)),
], ],
)), )),
if (T == Map || T == List) ...[ if (keyPath != "config" && (T == Map || T == List)) ...[
const PopupMenuItem<_OptionItems>( if (keyPath == "config.outbounds" && T == List) ...[
height: _popupMenuHeight, for (final String key in protocolSchemaValues.keys) ...{
padding: EdgeInsets.only(left: _popupMenuItemPadding), PopupMenuItem<_OptionItems>(
value: _OptionItems.string, height: _popupMenuHeight,
child: Row( padding: EdgeInsets.only(left: _popupMenuItemPadding),
mainAxisSize: MainAxisSize.min, value: key,
children: [ child: Row(
Icon(Icons.abc), mainAxisSize: MainAxisSize.min,
SizedBox(width: 10), children: [
Text("String", style: TextStyle(fontSize: 14)), Icon(Icons.data_object),
], SizedBox(width: 10),
Text(key, style: TextStyle(fontSize: 14)),
],
),
),
},
const PopupMenuDivider(height: 1),
],
if (T == Map) ...[
for (final String key in exampleSchemaValues.keys) ...{
if (keyPath == key)
for (final String key2 in exampleSchemaValues[key]!.keys) ...{
PopupMenuItem<_OptionItems>(
height: _popupMenuHeight,
padding: EdgeInsets.only(left: _popupMenuItemPadding),
value: key + "___" + key2,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.data_object),
SizedBox(width: 10),
Text(key2, style: TextStyle(fontSize: 14)),
],
),
),
},
const PopupMenuDivider(height: 1),
},
],
if (keyPath != "config" && !(T == List && keyPath == "config.outbounds")) ...[
const PopupMenuItem<_OptionItems>(
height: _popupMenuHeight,
padding: EdgeInsets.only(left: _popupMenuItemPadding),
value: "string",
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.abc),
SizedBox(width: 10),
Text("String", style: TextStyle(fontSize: 14)),
],
),
), ),
), const PopupMenuItem<_OptionItems>(
const PopupMenuItem<_OptionItems>( height: _popupMenuHeight,
height: _popupMenuHeight, padding: EdgeInsets.only(left: _popupMenuItemPadding),
padding: EdgeInsets.only(left: _popupMenuItemPadding), value: "num",
value: _OptionItems.num, child: Row(
child: Row( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ Icon(Icons.onetwothree),
Icon(Icons.onetwothree), SizedBox(width: 10),
SizedBox(width: 10), Text("Number", style: TextStyle(fontSize: 14)),
Text("Number", style: TextStyle(fontSize: 14)), ],
], ),
), ),
), const PopupMenuItem<_OptionItems>(
const PopupMenuItem<_OptionItems>( height: _popupMenuHeight,
height: _popupMenuHeight, padding: EdgeInsets.only(left: _popupMenuItemPadding),
padding: EdgeInsets.only(left: _popupMenuItemPadding), value: "bool",
value: _OptionItems.bool, child: Row(
child: Row( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ Icon(Icons.check_rounded),
Icon(Icons.check_rounded), SizedBox(width: 10),
SizedBox(width: 10), Text("Boolean", style: TextStyle(fontSize: 14)),
Text("Boolean", style: TextStyle(fontSize: 14)), ],
], ),
), ),
), const PopupMenuItem<_OptionItems>(
const PopupMenuItem<_OptionItems>( height: _popupMenuHeight,
height: _popupMenuHeight, padding: EdgeInsets.only(left: _popupMenuItemPadding),
padding: EdgeInsets.only(left: _popupMenuItemPadding), value: "map",
value: _OptionItems.map, child: Row(
child: Row( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ Icon(Icons.data_object),
Icon(Icons.data_object), SizedBox(width: 10),
SizedBox(width: 10), Text("object", style: TextStyle(fontSize: 14)),
Text("config", style: TextStyle(fontSize: 14)), ],
], ),
), ),
), const PopupMenuItem<_OptionItems>(
const PopupMenuItem<_OptionItems>( height: _popupMenuHeight,
height: _popupMenuHeight, padding: EdgeInsets.only(left: _popupMenuItemPadding),
padding: EdgeInsets.only(left: _popupMenuItemPadding), value: "list",
value: _OptionItems.list, child: Row(
child: Row( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ Icon(Icons.data_array),
Icon(Icons.data_array), SizedBox(width: 10),
SizedBox(width: 10), Text("List", style: TextStyle(fontSize: 14)),
Text("List", style: TextStyle(fontSize: 14)), ],
], ),
), ),
), ],
], ],
const PopupMenuDivider(height: 1), const PopupMenuDivider(height: 1),
const PopupMenuItem<_OptionItems>( if (keyPath != "config" && !(T == List && keyPath == "config.outbounds"))
height: _popupMenuHeight, const PopupMenuItem<_OptionItems>(
padding: EdgeInsets.only(left: 5), height: _popupMenuHeight,
value: _OptionItems.delete, padding: EdgeInsets.only(left: 5),
child: Row( value: "delete",
mainAxisSize: MainAxisSize.min, child: Row(
children: [ mainAxisSize: MainAxisSize.min,
Icon(Icons.delete), children: [
SizedBox(width: 10), Icon(Icons.delete),
Text("Delete", style: TextStyle(fontSize: 14)), SizedBox(width: 10),
], Text("Delete", style: TextStyle(fontSize: 14)),
],
),
), ),
),
]; ];
}, },
child: _options, child: _options,

Submodule libcore updated: 3305cf7d8d...5098951147