fix, settings on main window

Signed-off-by: dignow <linlong1265@gmail.com>
This commit is contained in:
dignow 2023-07-23 17:07:30 +08:00
parent d49dd9377e
commit b80051bb35
3 changed files with 347 additions and 356 deletions

View File

@ -1269,6 +1269,19 @@ String bool2option(String option, bool b) {
return res; return res;
} }
mainSetBoolOption(String key, bool value) async {
String v = bool2option(key, value);
await bind.mainSetOption(key: key, value: v);
}
Future<bool> mainGetBoolOption(String key) async {
return option2bool(key, await bind.mainGetOption(key: key));
}
bool mainGetBoolOptionSync(String key) {
return option2bool(key, bind.mainGetOptionSync(key: key));
}
Future<bool> matchPeer(String searchText, Peer peer) async { Future<bool> matchPeer(String searchText, Peer peer) async {
if (searchText.isEmpty) { if (searchText.isEmpty) {
return true; return true;

View File

@ -385,20 +385,17 @@ class _GeneralState extends State<_General> {
Widget record(BuildContext context) { Widget record(BuildContext context) {
return futureBuilder(future: () async { return futureBuilder(future: () async {
String customDirectory =
await bind.mainGetOption(key: 'video-save-directory');
String defaultDirectory = await bind.mainDefaultVideoSaveDirectory(); String defaultDirectory = await bind.mainDefaultVideoSaveDirectory();
String dir;
if (customDirectory.isNotEmpty) {
dir = customDirectory;
} else {
dir = defaultDirectory;
}
// canLaunchUrl blocked on windows portable, user SYSTEM // canLaunchUrl blocked on windows portable, user SYSTEM
return {'dir': dir, 'canlaunch': true}; return {'dir': defaultDirectory, 'canlaunch': true};
}(), hasData: (data) { }(), hasData: (data) {
Map<String, dynamic> map = data as Map<String, dynamic>; Map<String, dynamic> map = data as Map<String, dynamic>;
String dir = map['dir']!; String dir = map['dir']!;
String customDirectory =
bind.mainGetOptionSync(key: 'video-save-directory');
if (customDirectory.isNotEmpty) {
dir = customDirectory;
}
bool canlaunch = map['canlaunch']! as bool; bool canlaunch = map['canlaunch']! as bool;
return _Card(title: 'Recording', children: [ return _Card(title: 'Recording', children: [
@ -444,8 +441,7 @@ class _GeneralState extends State<_General> {
Widget language() { Widget language() {
return futureBuilder(future: () async { return futureBuilder(future: () async {
String langs = await bind.mainGetLangs(); String langs = await bind.mainGetLangs();
String lang = bind.mainGetLocalOption(key: kCommConfKeyLang); return {'langs': langs};
return {'langs': langs, 'lang': lang};
}(), hasData: (res) { }(), hasData: (res) {
Map<String, String> data = res as Map<String, String>; Map<String, String> data = res as Map<String, String>;
List<dynamic> langsList = jsonDecode(data['langs']!); List<dynamic> langsList = jsonDecode(data['langs']!);
@ -454,7 +450,7 @@ class _GeneralState extends State<_General> {
List<String> values = langsMap.values.toList(); List<String> values = langsMap.values.toList();
keys.insert(0, ''); keys.insert(0, '');
values.insert(0, translate('Default')); values.insert(0, translate('Default'));
String currentKey = data['lang']!; String currentKey = bind.mainGetLocalOption(key: kCommConfKeyLang);
if (!keys.contains(currentKey)) { if (!keys.contains(currentKey)) {
currentKey = ''; currentKey = '';
} }
@ -529,79 +525,75 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
Widget permissions(context) { Widget permissions(context) {
bool enabled = !locked; bool enabled = !locked;
return futureBuilder(future: () async { String accessMode = bind.mainGetOptionSync(key: 'access-mode');
return await bind.mainGetOption(key: 'access-mode'); _AccessMode mode;
}(), hasData: (data) { if (accessMode == 'full') {
String accessMode = data! as String; mode = _AccessMode.full;
_AccessMode mode; } else if (accessMode == 'view') {
if (accessMode == 'full') { mode = _AccessMode.view;
mode = _AccessMode.full; } else {
} else if (accessMode == 'view') { mode = _AccessMode.custom;
mode = _AccessMode.view; }
} else { String initialKey;
mode = _AccessMode.custom; bool? fakeValue;
} switch (mode) {
String initialKey; case _AccessMode.custom:
bool? fakeValue; initialKey = '';
switch (mode) { fakeValue = null;
case _AccessMode.custom: break;
initialKey = ''; case _AccessMode.full:
fakeValue = null; initialKey = 'full';
break; fakeValue = true;
case _AccessMode.full: break;
initialKey = 'full'; case _AccessMode.view:
fakeValue = true; initialKey = 'view';
break; fakeValue = false;
case _AccessMode.view: break;
initialKey = 'view'; }
fakeValue = false;
break;
}
return _Card(title: 'Permissions', children: [ return _Card(title: 'Permissions', children: [
_ComboBox( _ComboBox(
keys: [ keys: [
'', '',
'full', 'full',
'view', 'view',
],
values: [
translate('Custom'),
translate('Full Access'),
translate('Screen Share'),
],
enabled: enabled,
initialKey: initialKey,
onChanged: (mode) async {
await bind.mainSetOption(key: 'access-mode', value: mode);
setState(() {});
}).marginOnly(left: _kContentHMargin),
Column(
children: [
_OptionCheckBox(context, 'Enable Keyboard/Mouse', 'enable-keyboard',
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable Clipboard', 'enable-clipboard',
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(
context, 'Enable File Transfer', 'enable-file-transfer',
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable Audio', 'enable-audio',
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable TCP Tunneling', 'enable-tunnel',
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(
context, 'Enable Remote Restart', 'enable-remote-restart',
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(
context, 'Enable Recording Session', 'enable-record-session',
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable remote configuration modification',
'allow-remote-config-modification',
enabled: enabled, fakeValue: fakeValue),
], ],
), values: [
]); translate('Custom'),
}); translate('Full Access'),
translate('Screen Share'),
],
enabled: enabled,
initialKey: initialKey,
onChanged: (mode) async {
await bind.mainSetOption(key: 'access-mode', value: mode);
setState(() {});
}).marginOnly(left: _kContentHMargin),
Column(
children: [
_OptionCheckBox(context, 'Enable Keyboard/Mouse', 'enable-keyboard',
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable Clipboard', 'enable-clipboard',
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(
context, 'Enable File Transfer', 'enable-file-transfer',
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable Audio', 'enable-audio',
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable TCP Tunneling', 'enable-tunnel',
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(
context, 'Enable Remote Restart', 'enable-remote-restart',
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(
context, 'Enable Recording Session', 'enable-record-session',
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable remote configuration modification',
'allow-remote-config-modification',
enabled: enabled, fakeValue: fakeValue),
],
),
]);
} }
Widget password(BuildContext context) { Widget password(BuildContext context) {
@ -759,106 +751,94 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
return [ return [
_OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server', _OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server',
update: update, enabled: !locked), update: update, enabled: !locked),
futureBuilder( () {
future: () async { bool enabled = option2bool(
String enabled = await bind.mainGetOption(key: 'direct-server'); 'direct-server', bind.mainGetOptionSync(key: 'direct-server'));
String port = await bind.mainGetOption(key: 'direct-access-port'); if (!enabled) applyEnabled.value = false;
return {'enabled': enabled, 'port': port}; controller.text = bind.mainGetOptionSync(key: 'direct-access-port');
}(), return Offstage(
hasData: (data) { offstage: !enabled,
bool enabled = child: _SubLabeledWidget(
option2bool('direct-server', data['enabled'].toString()); context,
if (!enabled) applyEnabled.value = false; 'Port',
controller.text = data['port'].toString(); Row(children: [
return Offstage( SizedBox(
offstage: !enabled, width: 95,
child: _SubLabeledWidget( child: TextField(
context, controller: controller,
'Port', enabled: enabled && !locked,
Row(children: [ onChanged: (_) => applyEnabled.value = true,
SizedBox( inputFormatters: [
width: 95, FilteringTextInputFormatter.allow(RegExp(
child: TextField( r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
controller: controller, ],
enabled: enabled && !locked, decoration: const InputDecoration(
onChanged: (_) => applyEnabled.value = true, hintText: '21118',
inputFormatters: [ contentPadding:
FilteringTextInputFormatter.allow(RegExp( EdgeInsets.symmetric(vertical: 12, horizontal: 12),
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), ),
], ).marginOnly(right: 15),
decoration: const InputDecoration( ),
hintText: '21118', Obx(() => ElevatedButton(
contentPadding: onPressed: applyEnabled.value && enabled && !locked
EdgeInsets.symmetric(vertical: 12, horizontal: 12), ? () async {
applyEnabled.value = false;
await bind.mainSetOption(
key: 'direct-access-port',
value: controller.text);
}
: null,
child: Text(
translate('Apply'),
), ),
).marginOnly(right: 15), ))
), ]),
Obx(() => ElevatedButton( enabled: enabled && !locked,
onPressed: applyEnabled.value && enabled && !locked ),
? () async { );
applyEnabled.value = false; }(),
await bind.mainSetOption(
key: 'direct-access-port',
value: controller.text);
}
: null,
child: Text(
translate('Apply'),
),
))
]),
enabled: enabled && !locked,
),
);
},
),
]; ];
} }
Widget whitelist() { Widget whitelist() {
bool enabled = !locked; bool enabled = !locked;
return futureBuilder(future: () async { RxBool hasWhitelist =
return await bind.mainGetOption(key: 'whitelist'); bind.mainGetOptionSync(key: 'whitelist').isNotEmpty.obs;
}(), hasData: (data) { update() async {
RxBool hasWhitelist = (data as String).isNotEmpty.obs; hasWhitelist.value = bind.mainGetOptionSync(key: 'whitelist').isNotEmpty;
update() async { }
hasWhitelist.value =
(await bind.mainGetOption(key: 'whitelist')).isNotEmpty;
}
onChanged(bool? checked) async { onChanged(bool? checked) async {
changeWhiteList(callback: update); changeWhiteList(callback: update);
} }
return GestureDetector( return GestureDetector(
child: Tooltip( child: Tooltip(
message: translate('whitelist_tip'), message: translate('whitelist_tip'),
child: Obx(() => Row( child: Obx(() => Row(
children: [ children: [
Checkbox( Checkbox(
value: hasWhitelist.value, value: hasWhitelist.value,
onChanged: enabled ? onChanged : null) onChanged: enabled ? onChanged : null)
.marginOnly(right: 5),
Offstage(
offstage: !hasWhitelist.value,
child: const Icon(Icons.warning_amber_rounded,
color: Color.fromARGB(255, 255, 204, 0))
.marginOnly(right: 5), .marginOnly(right: 5),
Offstage( ),
offstage: !hasWhitelist.value, Expanded(
child: const Icon(Icons.warning_amber_rounded, child: Text(
color: Color.fromARGB(255, 255, 204, 0)) translate('Use IP Whitelisting'),
.marginOnly(right: 5), style: TextStyle(color: _disabledTextColor(context, enabled)),
), ))
Expanded( ],
child: Text( )),
translate('Use IP Whitelisting'), ),
style: onTap: () {
TextStyle(color: _disabledTextColor(context, enabled)), onChanged(!hasWhitelist.value);
)) },
], ).marginOnly(left: _kCheckBoxLeftMargin);
)),
),
onTap: () {
onChanged(!hasWhitelist.value);
},
).marginOnly(left: _kCheckBoxLeftMargin);
});
} }
Widget hide_cm(bool enabled) { Widget hide_cm(bool enabled) {
@ -943,154 +923,150 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
} }
server(bool enabled) { server(bool enabled) {
return futureBuilder(future: () async { // Setting page is not modal, oldOptions should only be used when getting options, never when setting.
return await bind.mainGetOptions(); Map<String, dynamic> oldOptions =
}(), hasData: (data) { jsonDecode(bind.mainGetOptionsSync() as String);
// Setting page is not modal, oldOptions should only be used when getting options, never when setting. old(String key) {
Map<String, dynamic> oldOptions = jsonDecode(data! as String); return (oldOptions[key] ?? '').trim();
old(String key) { }
return (oldOptions[key] ?? '').trim();
}
RxString idErrMsg = ''.obs; RxString idErrMsg = ''.obs;
RxString relayErrMsg = ''.obs; RxString relayErrMsg = ''.obs;
RxString apiErrMsg = ''.obs; RxString apiErrMsg = ''.obs;
var idController = var idController =
TextEditingController(text: old('custom-rendezvous-server')); TextEditingController(text: old('custom-rendezvous-server'));
var relayController = TextEditingController(text: old('relay-server')); var relayController = TextEditingController(text: old('relay-server'));
var apiController = TextEditingController(text: old('api-server')); var apiController = TextEditingController(text: old('api-server'));
var keyController = TextEditingController(text: old('key')); var keyController = TextEditingController(text: old('key'));
set(String idServer, String relayServer, String apiServer, set(String idServer, String relayServer, String apiServer,
String key) async { String key) async {
idServer = idServer.trim(); idServer = idServer.trim();
relayServer = relayServer.trim(); relayServer = relayServer.trim();
apiServer = apiServer.trim(); apiServer = apiServer.trim();
key = key.trim(); key = key.trim();
if (idServer.isNotEmpty) { if (idServer.isNotEmpty) {
idErrMsg.value = idErrMsg.value =
translate(await bind.mainTestIfValidServer(server: idServer)); translate(await bind.mainTestIfValidServer(server: idServer));
if (idErrMsg.isNotEmpty) { if (idErrMsg.isNotEmpty) {
return false; return false;
}
}
if (relayServer.isNotEmpty) {
relayErrMsg.value =
translate(await bind.mainTestIfValidServer(server: relayServer));
if (relayErrMsg.isNotEmpty) {
return false;
}
}
if (apiServer.isNotEmpty) {
if (!apiServer.startsWith('http://') &&
!apiServer.startsWith('https://')) {
apiErrMsg.value =
'${translate("API Server")}: ${translate("invalid_http")}';
return false;
}
}
final old = await bind.mainGetOption(key: 'custom-rendezvous-server');
if (old.isNotEmpty && old != idServer) {
await gFFI.userModel.logOut();
}
// should set one by one
await bind.mainSetOption(
key: 'custom-rendezvous-server', value: idServer);
await bind.mainSetOption(key: 'relay-server', value: relayServer);
await bind.mainSetOption(key: 'api-server', value: apiServer);
await bind.mainSetOption(key: 'key', value: key);
return true;
}
submit() async {
bool result = await set(idController.text, relayController.text,
apiController.text, keyController.text);
if (result) {
setState(() {});
showToast(translate('Successful'));
} else {
showToast(translate('Failed'));
} }
} }
if (relayServer.isNotEmpty) {
relayErrMsg.value =
translate(await bind.mainTestIfValidServer(server: relayServer));
if (relayErrMsg.isNotEmpty) {
return false;
}
}
if (apiServer.isNotEmpty) {
if (!apiServer.startsWith('http://') &&
!apiServer.startsWith('https://')) {
apiErrMsg.value =
'${translate("API Server")}: ${translate("invalid_http")}';
return false;
}
}
final old = await bind.mainGetOption(key: 'custom-rendezvous-server');
if (old.isNotEmpty && old != idServer) {
await gFFI.userModel.logOut();
}
// should set one by one
await bind.mainSetOption(
key: 'custom-rendezvous-server', value: idServer);
await bind.mainSetOption(key: 'relay-server', value: relayServer);
await bind.mainSetOption(key: 'api-server', value: apiServer);
await bind.mainSetOption(key: 'key', value: key);
return true;
}
import() { submit() async {
Clipboard.getData(Clipboard.kTextPlain).then((value) { bool result = await set(idController.text, relayController.text,
final text = value?.text; apiController.text, keyController.text);
if (text != null && text.isNotEmpty) { if (result) {
try { setState(() {});
final sc = ServerConfig.decode(text); showToast(translate('Successful'));
if (sc.idServer.isNotEmpty) { } else {
idController.text = sc.idServer; showToast(translate('Failed'));
relayController.text = sc.relayServer; }
apiController.text = sc.apiServer; }
keyController.text = sc.key;
Future<bool> success = import() {
set(sc.idServer, sc.relayServer, sc.apiServer, sc.key); Clipboard.getData(Clipboard.kTextPlain).then((value) {
success.then((value) { final text = value?.text;
if (value) { if (text != null && text.isNotEmpty) {
showToast( try {
translate('Import server configuration successfully')); final sc = ServerConfig.decode(text);
} else { if (sc.idServer.isNotEmpty) {
showToast(translate('Invalid server configuration')); idController.text = sc.idServer;
} relayController.text = sc.relayServer;
}); apiController.text = sc.apiServer;
} else { keyController.text = sc.key;
showToast(translate('Invalid server configuration')); Future<bool> success =
} set(sc.idServer, sc.relayServer, sc.apiServer, sc.key);
} catch (e) { success.then((value) {
if (value) {
showToast(
translate('Import server configuration successfully'));
} else {
showToast(translate('Invalid server configuration'));
}
});
} else {
showToast(translate('Invalid server configuration')); showToast(translate('Invalid server configuration'));
} }
} else { } catch (e) {
showToast(translate('Clipboard is empty')); showToast(translate('Invalid server configuration'));
} }
}); } else {
} showToast(translate('Clipboard is empty'));
}
});
}
export() { export() {
final text = ServerConfig( final text = ServerConfig(
idServer: idController.text, idServer: idController.text,
relayServer: relayController.text, relayServer: relayController.text,
apiServer: apiController.text, apiServer: apiController.text,
key: keyController.text) key: keyController.text)
.encode(); .encode();
debugPrint("ServerConfig export: $text"); debugPrint("ServerConfig export: $text");
Clipboard.setData(ClipboardData(text: text)); Clipboard.setData(ClipboardData(text: text));
showToast(translate('Export server configuration successfully')); showToast(translate('Export server configuration successfully'));
} }
bool secure = !enabled; bool secure = !enabled;
return _Card(title: 'ID/Relay Server', title_suffix: [ return _Card(title: 'ID/Relay Server', title_suffix: [
Tooltip( Tooltip(
message: translate('Import Server Config'), message: translate('Import Server Config'),
child: IconButton(
icon: Icon(Icons.paste, color: Colors.grey),
onPressed: enabled ? import : null),
),
Tooltip(
message: translate('Export Server Config'),
child: IconButton( child: IconButton(
icon: Icon(Icons.paste, color: Colors.grey), icon: Icon(Icons.copy, color: Colors.grey),
onPressed: enabled ? import : null), onPressed: enabled ? export : null)),
), ], children: [
Tooltip( Column(
message: translate('Export Server Config'), children: [
child: IconButton( Obx(() => _LabeledTextField(context, 'ID Server', idController,
icon: Icon(Icons.copy, color: Colors.grey), idErrMsg.value, enabled, secure)),
onPressed: enabled ? export : null)), Obx(() => _LabeledTextField(context, 'Relay Server', relayController,
], children: [ relayErrMsg.value, enabled, secure)),
Column( Obx(() => _LabeledTextField(context, 'API Server', apiController,
children: [ apiErrMsg.value, enabled, secure)),
Obx(() => _LabeledTextField(context, 'ID Server', idController, _LabeledTextField(context, 'Key', keyController, '', enabled, secure),
idErrMsg.value, enabled, secure)), Row(
Obx(() => _LabeledTextField(context, 'Relay Server', mainAxisAlignment: MainAxisAlignment.end,
relayController, relayErrMsg.value, enabled, secure)), children: [_Button('Apply', submit, enabled: enabled)],
Obx(() => _LabeledTextField(context, 'API Server', apiController, ).marginOnly(top: 10),
apiErrMsg.value, enabled, secure)), ],
_LabeledTextField( )
context, 'Key', keyController, '', enabled, secure), ]);
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [_Button('Apply', submit, enabled: enabled)],
).marginOnly(top: 10),
],
)
]);
});
} }
} }
@ -1662,53 +1638,47 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
bool enabled = true, bool enabled = true,
Icon? checkedIcon, Icon? checkedIcon,
bool? fakeValue}) { bool? fakeValue}) {
return futureBuilder( bool value = mainGetBoolOptionSync(key);
future: bind.mainGetOption(key: key), if (reverse) value = !value;
hasData: (data) { var ref = value.obs;
bool value = option2bool(key, data.toString()); onChanged(option) async {
if (reverse) value = !value; if (option != null) {
var ref = value.obs; if (reverse) option = !option;
onChanged(option) async { await mainSetBoolOption(key, option);
if (option != null) { ref.value = mainGetBoolOptionSync(key);
ref.value = option; update?.call();
if (reverse) option = !option; }
String value = bool2option(key, option); }
await bind.mainSetOption(key: key, value: value);
update?.call(); if (fakeValue != null) {
ref.value = fakeValue;
enabled = false;
}
return GestureDetector(
child: Obx(
() => Row(
children: [
Checkbox(value: ref.value, onChanged: enabled ? onChanged : null)
.marginOnly(right: 5),
Offstage(
offstage: !ref.value || checkedIcon == null,
child: checkedIcon?.marginOnly(right: 5),
),
Expanded(
child: Text(
translate(label),
style: TextStyle(color: _disabledTextColor(context, enabled)),
))
],
),
).marginOnly(left: _kCheckBoxLeftMargin),
onTap: enabled
? () {
onChanged(!ref.value);
} }
} : null,
);
if (fakeValue != null) {
ref.value = fakeValue;
enabled = false;
}
return GestureDetector(
child: Obx(
() => Row(
children: [
Checkbox(
value: ref.value, onChanged: enabled ? onChanged : null)
.marginOnly(right: 5),
Offstage(
offstage: !ref.value || checkedIcon == null,
child: checkedIcon?.marginOnly(right: 5),
),
Expanded(
child: Text(
translate(label),
style: TextStyle(color: _disabledTextColor(context, enabled)),
))
],
),
).marginOnly(left: _kCheckBoxLeftMargin),
onTap: enabled
? () {
onChanged(!ref.value);
}
: null,
);
});
} }
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names

View File

@ -606,6 +606,10 @@ pub fn main_get_option(key: String) -> String {
get_option(key) get_option(key)
} }
pub fn main_get_option_sync(key: String) -> SyncReturn<String> {
SyncReturn(get_option(key))
}
pub fn main_get_error() -> String { pub fn main_get_error() -> String {
get_error() get_error()
} }
@ -626,6 +630,10 @@ pub fn main_get_options() -> String {
get_options() get_options()
} }
pub fn main_get_options_sync() -> SyncReturn<String> {
SyncReturn(get_options())
}
pub fn main_set_options(json: String) { pub fn main_set_options(json: String) {
let map: HashMap<String, String> = serde_json::from_str(&json).unwrap_or(HashMap::new()); let map: HashMap<String, String> = serde_json::from_str(&json).unwrap_or(HashMap::new());
if !map.is_empty() { if !map.is_empty() {