feat: ip whitelist, id/relay server/ socks5 proxy, about page
This commit is contained in:
parent
b1382c2d57
commit
08043732a8
@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart' hide MenuItem;
|
import 'package:flutter/material.dart' hide MenuItem;
|
||||||
@ -9,6 +10,7 @@ import 'package:flutter_hbb/models/model.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tray_manager/tray_manager.dart';
|
import 'package:tray_manager/tray_manager.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class DesktopHomePage extends StatefulWidget {
|
class DesktopHomePage extends StatefulWidget {
|
||||||
DesktopHomePage({Key? key}) : super(key: key);
|
DesktopHomePage({Key? key}) : super(key: key);
|
||||||
@ -105,33 +107,78 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
translate("ID"),
|
translate("ID"),
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
|
style: TextStyle(
|
||||||
|
fontSize: 18, fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
padding: EdgeInsets.all(4.0),
|
padding: EdgeInsets.all(4.0),
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
genEnablePopupMenuItem(translate("Enable Keyboard/Mouse"), 'enable-keyboard',),
|
genEnablePopupMenuItem(
|
||||||
genEnablePopupMenuItem(translate("Enable Clipboard"), 'enable-clipboard',),
|
translate("Enable Keyboard/Mouse"),
|
||||||
genEnablePopupMenuItem(translate("Enable File Transfer"), 'enable-file-transfer',),
|
'enable-keyboard',
|
||||||
genEnablePopupMenuItem(translate("Enable TCP Tunneling"), 'enable-tunnel',),
|
),
|
||||||
genAudioInputPopupMenuItem(),
|
genEnablePopupMenuItem(
|
||||||
// TODO: Audio Input
|
translate("Enable Clipboard"),
|
||||||
PopupMenuItem(child: Text(translate("ID/Relay Server")), value: 'custom-server',),
|
'enable-clipboard',
|
||||||
PopupMenuItem(child: Text(translate("IP Whitelisting")), value: 'whitelist',),
|
),
|
||||||
PopupMenuItem(child: Text(translate("Socks5 Proxy")), value: 'Socks5 Proxy',),
|
genEnablePopupMenuItem(
|
||||||
// sep
|
translate("Enable File Transfer"),
|
||||||
genEnablePopupMenuItem(translate("Enable Service"), 'stop-service',),
|
'enable-file-transfer',
|
||||||
// TODO: direct server
|
),
|
||||||
genEnablePopupMenuItem(translate("Always connected via relay"),'allow-always-relay',),
|
genEnablePopupMenuItem(
|
||||||
genEnablePopupMenuItem(translate("Start ID/relay service"),'stop-rendezvous-service',),
|
translate("Enable TCP Tunneling"),
|
||||||
PopupMenuItem(child: Text(translate("Change ID")), value: 'change-id',),
|
'enable-tunnel',
|
||||||
genEnablePopupMenuItem(translate("Dark Theme"), 'allow-darktheme',),
|
),
|
||||||
PopupMenuItem(child: Text(translate("About")), value: 'about',),
|
genAudioInputPopupMenuItem(),
|
||||||
], onSelected: onSelectMenu,)
|
// TODO: Audio Input
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text(translate("ID/Relay Server")),
|
||||||
|
value: 'custom-server',
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text(translate("IP Whitelisting")),
|
||||||
|
value: 'whitelist',
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text(translate("Socks5 Proxy")),
|
||||||
|
value: 'socks5-proxy',
|
||||||
|
),
|
||||||
|
// sep
|
||||||
|
genEnablePopupMenuItem(
|
||||||
|
translate("Enable Service"),
|
||||||
|
'stop-service',
|
||||||
|
),
|
||||||
|
// TODO: direct server
|
||||||
|
genEnablePopupMenuItem(
|
||||||
|
translate("Always connected via relay"),
|
||||||
|
'allow-always-relay',
|
||||||
|
),
|
||||||
|
genEnablePopupMenuItem(
|
||||||
|
translate("Start ID/relay service"),
|
||||||
|
'stop-rendezvous-service',
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text(translate("Change ID")),
|
||||||
|
value: 'change-id',
|
||||||
|
),
|
||||||
|
genEnablePopupMenuItem(
|
||||||
|
translate("Dark Theme"),
|
||||||
|
'allow-darktheme',
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text(translate("About")),
|
||||||
|
value: 'about',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onSelected: onSelectMenu,
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: model.serverId,
|
controller: model.serverId,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
enabled: false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -268,80 +315,111 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
|
|||||||
gFFI.setOption(value, option == "Y" ? "" : "Y");
|
gFFI.setOption(value, option == "Y" ? "" : "Y");
|
||||||
} else if (value == "change-id") {
|
} else if (value == "change-id") {
|
||||||
changeId();
|
changeId();
|
||||||
|
} else if (value == "custom-server") {
|
||||||
|
changeServer();
|
||||||
|
} else if (value == "whitelist") {
|
||||||
|
changeWhiteList();
|
||||||
|
} else if (value == "socks5-proxy") {
|
||||||
|
changeSocks5Proxy();
|
||||||
|
} else if (value == "about") {
|
||||||
|
about();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PopupMenuItem<String> genEnablePopupMenuItem(String label, String value) {
|
PopupMenuItem<String> genEnablePopupMenuItem(String label, String value) {
|
||||||
final isEnable =
|
final isEnable = label.startsWith('enable-')
|
||||||
label.startsWith('enable-') ? gFFI.getOption(value) != "N" : gFFI.getOption(value) != "Y";
|
? gFFI.getOption(value) != "N"
|
||||||
return PopupMenuItem(child: Row(
|
: gFFI.getOption(value) != "Y";
|
||||||
children: [
|
return PopupMenuItem(
|
||||||
Offstage(offstage: !isEnable, child: Icon(Icons.check)),
|
child: Row(
|
||||||
Text(label, style: genTextStyle(isEnable),),
|
children: [
|
||||||
],
|
Offstage(offstage: !isEnable, child: Icon(Icons.check)),
|
||||||
), value: value,);
|
Text(
|
||||||
|
label,
|
||||||
|
style: genTextStyle(isEnable),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
value: value,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TextStyle genTextStyle(bool isPositive) {
|
TextStyle genTextStyle(bool isPositive) {
|
||||||
return isPositive ? TextStyle() : TextStyle(
|
return isPositive
|
||||||
color: Colors.redAccent,
|
? TextStyle()
|
||||||
decoration: TextDecoration.lineThrough
|
: TextStyle(
|
||||||
);
|
color: Colors.redAccent, decoration: TextDecoration.lineThrough);
|
||||||
}
|
}
|
||||||
|
|
||||||
PopupMenuItem<String> genAudioInputPopupMenuItem() {
|
PopupMenuItem<String> genAudioInputPopupMenuItem() {
|
||||||
final _enabledInput = gFFI.getOption('enable-audio');
|
final _enabledInput = gFFI.getOption('enable-audio');
|
||||||
var defaultInput = gFFI.getDefaultAudioInput().obs;
|
var defaultInput = gFFI.getDefaultAudioInput().obs;
|
||||||
var enabled = (_enabledInput != "N").obs;
|
var enabled = (_enabledInput != "N").obs;
|
||||||
return PopupMenuItem(child: FutureBuilder<List<String>>(
|
return PopupMenuItem(
|
||||||
future: gFFI.getAudioInputs(),
|
child: FutureBuilder<List<String>>(
|
||||||
builder: (context, snapshot) {
|
future: gFFI.getAudioInputs(),
|
||||||
if (snapshot.hasData) {
|
builder: (context, snapshot) {
|
||||||
final inputs = snapshot.data!;
|
if (snapshot.hasData) {
|
||||||
if (Platform.isWindows) {
|
final inputs = snapshot.data!;
|
||||||
inputs.insert(0, translate("System Sound"));
|
if (Platform.isWindows) {
|
||||||
}
|
inputs.insert(0, translate("System Sound"));
|
||||||
var inputList = inputs.map((e) => PopupMenuItem(
|
}
|
||||||
child: Row(
|
var inputList = inputs
|
||||||
children: [
|
.map((e) => PopupMenuItem(
|
||||||
Obx(()=> Offstage(offstage: defaultInput.value != e, child: Icon(Icons.check))),
|
child: Row(
|
||||||
Expanded(child: Tooltip(
|
children: [
|
||||||
message: e,
|
Obx(() => Offstage(
|
||||||
child: Text("$e",maxLines: 1, overflow: TextOverflow.ellipsis,))),
|
offstage: defaultInput.value != e,
|
||||||
],
|
child: Icon(Icons.check))),
|
||||||
),
|
Expanded(
|
||||||
value: e,
|
child: Tooltip(
|
||||||
)).toList();
|
message: e,
|
||||||
inputList.insert(0, PopupMenuItem(
|
child: Text(
|
||||||
child: Row(
|
"$e",
|
||||||
children: [
|
maxLines: 1,
|
||||||
Obx(()=> Offstage(offstage: enabled.value, child: Icon(Icons.check))),
|
overflow: TextOverflow.ellipsis,
|
||||||
Expanded(child: Text(translate("Mute"))),
|
))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
value: "Mute",
|
value: e,
|
||||||
));
|
))
|
||||||
return PopupMenuButton<String>(
|
.toList();
|
||||||
|
inputList.insert(
|
||||||
|
0,
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Obx(() => Offstage(
|
||||||
|
offstage: enabled.value, child: Icon(Icons.check))),
|
||||||
|
Expanded(child: Text(translate("Mute"))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
value: "Mute",
|
||||||
|
));
|
||||||
|
return PopupMenuButton<String>(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
child: Container(
|
child: Container(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text(translate("Audio Input"))),
|
child: Text(translate("Audio Input"))),
|
||||||
itemBuilder: (context) => inputList,
|
itemBuilder: (context) => inputList,
|
||||||
onSelected: (dev) {
|
onSelected: (dev) {
|
||||||
if (dev == "Mute") {
|
if (dev == "Mute") {
|
||||||
gFFI.setOption('enable-audio', _enabledInput == 'N' ? '': 'N');
|
gFFI.setOption(
|
||||||
enabled.value = gFFI.getOption('enable-audio') != 'N';
|
'enable-audio', _enabledInput == 'N' ? '' : 'N');
|
||||||
} else if (dev != gFFI.getDefaultAudioInput()) {
|
enabled.value = gFFI.getOption('enable-audio') != 'N';
|
||||||
gFFI.setDefaultAudioInput(dev);
|
} else if (dev != gFFI.getDefaultAudioInput()) {
|
||||||
defaultInput.value = dev;
|
gFFI.setDefaultAudioInput(dev);
|
||||||
}
|
defaultInput.value = dev;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Text("...");
|
return Text("...");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
), value: 'audio-input',);
|
),
|
||||||
|
value: 'audio-input',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// change local ID
|
/// change local ID
|
||||||
@ -349,27 +427,30 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
|
|||||||
var newId = "";
|
var newId = "";
|
||||||
var msg = "";
|
var msg = "";
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
DialogManager.show( (setState, close) {
|
DialogManager.show((setState, close) {
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate("Change ID")),
|
title: Text(translate("Change ID")),
|
||||||
content: Column(
|
content: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(translate("id_change_tip")),
|
Text(translate("id_change_tip")),
|
||||||
SizedBox(height: 8.0,),
|
SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text("ID:").marginOnly(bottom: 16.0),
|
Text("ID:").marginOnly(bottom: 16.0),
|
||||||
SizedBox(width: 24.0,),
|
SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
onChanged: (s) {
|
||||||
newId = s;
|
newId = s;
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
errorText: msg.isEmpty ? null : translate(msg)
|
errorText: msg.isEmpty ? null : translate(msg)),
|
||||||
),
|
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
LengthLimitingTextInputFormatter(16),
|
LengthLimitingTextInputFormatter(16),
|
||||||
// FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true)
|
// FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true)
|
||||||
@ -379,34 +460,546 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 4.0,),
|
SizedBox(
|
||||||
Offstage(
|
height: 4.0,
|
||||||
offstage: !isInProgress,
|
),
|
||||||
child: LinearProgressIndicator())
|
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||||
],
|
],
|
||||||
), actions: [
|
),
|
||||||
TextButton(onPressed: (){
|
actions: [
|
||||||
close();
|
TextButton(
|
||||||
}, child: Text("取消")),
|
onPressed: () {
|
||||||
TextButton(onPressed: () async {
|
close();
|
||||||
setState(() {
|
},
|
||||||
msg = "";
|
child: Text(translate("Cancel"))),
|
||||||
isInProgress = true;
|
TextButton(
|
||||||
gFFI.bind.mainChangeId(newId: newId);
|
onPressed: () async {
|
||||||
});
|
setState(() {
|
||||||
|
msg = "";
|
||||||
|
isInProgress = true;
|
||||||
|
gFFI.bind.mainChangeId(newId: newId);
|
||||||
|
});
|
||||||
|
|
||||||
var status = await gFFI.bind.mainGetAsyncStatus();
|
var status = await gFFI.bind.mainGetAsyncStatus();
|
||||||
while (status == " "){
|
while (status == " ") {
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
status = await gFFI.bind.mainGetAsyncStatus();
|
status = await gFFI.bind.mainGetAsyncStatus();
|
||||||
}
|
}
|
||||||
setState(() {
|
if (status.isEmpty) {
|
||||||
isInProgress = false;
|
// ok
|
||||||
msg = translate(status);
|
close();
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isInProgress = false;
|
||||||
|
msg = translate(status);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}, child: Text("确定")),
|
void changeServer() async {
|
||||||
],
|
Map<String, dynamic> oldOptions =
|
||||||
|
jsonDecode(await gFFI.bind.mainGetOptions());
|
||||||
|
print("${oldOptions}");
|
||||||
|
String idServer = oldOptions['custom-rendezvous-server'] ?? "";
|
||||||
|
var idServerMsg = "";
|
||||||
|
String relayServer = oldOptions['relay-server'] ?? "";
|
||||||
|
var relayServerMsg = "";
|
||||||
|
String apiServer = oldOptions['api-server'] ?? "";
|
||||||
|
var apiServerMsg = "";
|
||||||
|
var key = oldOptions['key'] ?? "";
|
||||||
|
|
||||||
|
var isInProgress = false;
|
||||||
|
DialogManager.show((setState, close) {
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("ID/Relay Server")),
|
||||||
|
content: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: 500),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: 100),
|
||||||
|
child: Text("${translate('ID Server')}:")
|
||||||
|
.marginOnly(bottom: 16.0)),
|
||||||
|
SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
onChanged: (s) {
|
||||||
|
idServer = s;
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
errorText:
|
||||||
|
idServerMsg.isNotEmpty ? idServerMsg : null),
|
||||||
|
controller: TextEditingController(text: idServer),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: 100),
|
||||||
|
child: Text("${translate('Relay Server')}:")
|
||||||
|
.marginOnly(bottom: 16.0)),
|
||||||
|
SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
onChanged: (s) {
|
||||||
|
relayServer = s;
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
errorText: relayServerMsg.isNotEmpty
|
||||||
|
? relayServerMsg
|
||||||
|
: null),
|
||||||
|
controller: TextEditingController(text: relayServer),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: 100),
|
||||||
|
child: Text("${translate('API Server')}:")
|
||||||
|
.marginOnly(bottom: 16.0)),
|
||||||
|
SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
onChanged: (s) {
|
||||||
|
apiServer = s;
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
errorText:
|
||||||
|
apiServerMsg.isNotEmpty ? apiServerMsg : null),
|
||||||
|
controller: TextEditingController(text: apiServer),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: 100),
|
||||||
|
child: Text("${translate('Key')}:")
|
||||||
|
.marginOnly(bottom: 16.0)),
|
||||||
|
SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
onChanged: (s) {
|
||||||
|
key = s;
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
controller: TextEditingController(text: key),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 4.0,
|
||||||
|
),
|
||||||
|
Offstage(
|
||||||
|
offstage: !isInProgress, child: LinearProgressIndicator())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
child: Text(translate("Cancel"))),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
[idServerMsg, relayServerMsg, apiServerMsg]
|
||||||
|
.forEach((element) {
|
||||||
|
element = "";
|
||||||
|
});
|
||||||
|
isInProgress = true;
|
||||||
|
});
|
||||||
|
final cancel = () {
|
||||||
|
setState(() {
|
||||||
|
isInProgress = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
idServer = idServer.trim();
|
||||||
|
relayServer = relayServer.trim();
|
||||||
|
apiServer = apiServer.trim();
|
||||||
|
key = key.trim();
|
||||||
|
|
||||||
|
if (idServer.isNotEmpty) {
|
||||||
|
idServerMsg = translate(
|
||||||
|
await gFFI.bind.mainTestIfValidServer(server: idServer));
|
||||||
|
if (idServerMsg.isEmpty) {
|
||||||
|
oldOptions['custom-rendezvous-server'] = idServer;
|
||||||
|
} else {
|
||||||
|
cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
oldOptions['custom-rendezvous-server'] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relayServer.isNotEmpty) {
|
||||||
|
relayServerMsg = translate(await gFFI.bind
|
||||||
|
.mainTestIfValidServer(server: relayServer));
|
||||||
|
if (relayServerMsg.isEmpty) {
|
||||||
|
oldOptions['relay-server'] = relayServer;
|
||||||
|
} else {
|
||||||
|
cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
oldOptions['relay-server'] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiServer.isNotEmpty) {
|
||||||
|
if (apiServer.startsWith('http://') ||
|
||||||
|
apiServer.startsWith("https://")) {
|
||||||
|
oldOptions['api-server'] = apiServer;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
apiServerMsg = translate("invalid_http");
|
||||||
|
cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
oldOptions['api-server'] = "";
|
||||||
|
}
|
||||||
|
// ok
|
||||||
|
oldOptions['key'] = key;
|
||||||
|
await gFFI.bind.mainSetOptions(json: jsonEncode(oldOptions));
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void changeWhiteList() async {
|
||||||
|
Map<String, dynamic> oldOptions =
|
||||||
|
jsonDecode(await gFFI.bind.mainGetOptions());
|
||||||
|
var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(',');
|
||||||
|
var newWhiteListField = newWhiteList.join('\n');
|
||||||
|
var msg = "";
|
||||||
|
var isInProgress = false;
|
||||||
|
DialogManager.show((setState, close) {
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("IP Whitelisting")),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(translate("whitelist_sep")),
|
||||||
|
SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
onChanged: (s) {
|
||||||
|
newWhiteListField = s;
|
||||||
|
},
|
||||||
|
maxLines: null,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
errorText: msg.isEmpty ? null : translate(msg),
|
||||||
|
),
|
||||||
|
controller: TextEditingController(text: newWhiteListField),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 4.0,
|
||||||
|
),
|
||||||
|
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
child: Text(translate("Cancel"))),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
msg = "";
|
||||||
|
isInProgress = true;
|
||||||
|
});
|
||||||
|
newWhiteListField = newWhiteListField.trim();
|
||||||
|
var newWhiteList = "";
|
||||||
|
if (newWhiteListField.isEmpty) {
|
||||||
|
// pass
|
||||||
|
} else {
|
||||||
|
final ips =
|
||||||
|
newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
|
||||||
|
// test ip
|
||||||
|
final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$");
|
||||||
|
for (final ip in ips) {
|
||||||
|
if (!ipMatch.hasMatch(ip)) {
|
||||||
|
msg = translate("Invalid IP") + " $ip";
|
||||||
|
setState(() {
|
||||||
|
isInProgress = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newWhiteList = ips.join(',');
|
||||||
|
}
|
||||||
|
oldOptions['whitelist'] = newWhiteList;
|
||||||
|
await gFFI.bind.mainSetOptions(json: jsonEncode(oldOptions));
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void changeSocks5Proxy() async {
|
||||||
|
var socks = await gFFI.bind.mainGetSocks();
|
||||||
|
|
||||||
|
String proxy = "";
|
||||||
|
String proxyMsg = "";
|
||||||
|
String username = "";
|
||||||
|
String password = "";
|
||||||
|
if (socks.length == 3) {
|
||||||
|
proxy = socks[0];
|
||||||
|
username = socks[1];
|
||||||
|
password = socks[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
var isInProgress = false;
|
||||||
|
DialogManager.show((setState, close) {
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("Socks5 Proxy")),
|
||||||
|
content: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: 500),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: 100),
|
||||||
|
child: Text("${translate('Hostname')}:")
|
||||||
|
.marginOnly(bottom: 16.0)),
|
||||||
|
SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
onChanged: (s) {
|
||||||
|
proxy = s;
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
errorText:
|
||||||
|
proxyMsg.isNotEmpty ? proxyMsg : null),
|
||||||
|
controller: TextEditingController(text: proxy),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: 100),
|
||||||
|
child: Text("${translate('Username')}:")
|
||||||
|
.marginOnly(bottom: 16.0)),
|
||||||
|
SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
onChanged: (s) {
|
||||||
|
username = s;
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
controller: TextEditingController(text: username),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: 100),
|
||||||
|
child: Text("${translate('Password')}:")
|
||||||
|
.marginOnly(bottom: 16.0)),
|
||||||
|
SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
onChanged: (s) {
|
||||||
|
password = s;
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
controller: TextEditingController(text: password),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Offstage(
|
||||||
|
offstage: !isInProgress, child: LinearProgressIndicator())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
child: Text(translate("Cancel"))),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
proxyMsg = "";
|
||||||
|
isInProgress = true;
|
||||||
|
});
|
||||||
|
final cancel = () {
|
||||||
|
setState(() {
|
||||||
|
isInProgress = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
proxy = proxy.trim();
|
||||||
|
username = username.trim();
|
||||||
|
password = password.trim();
|
||||||
|
|
||||||
|
if (proxy.isNotEmpty) {
|
||||||
|
proxyMsg = translate(
|
||||||
|
await gFFI.bind.mainTestIfValidServer(server: proxy));
|
||||||
|
if (proxyMsg.isEmpty) {
|
||||||
|
// ignore
|
||||||
|
} else {
|
||||||
|
cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await gFFI.bind.mainSetSocks(proxy: proxy, username: username, password: password);
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void about() async {
|
||||||
|
final appName = await gFFI.bind.mainGetAppName();
|
||||||
|
final license = await gFFI.bind.mainGetLicense();
|
||||||
|
final version = await gFFI.bind.mainGetVersion();
|
||||||
|
final linkStyle = TextStyle(
|
||||||
|
decoration: TextDecoration.underline
|
||||||
|
);
|
||||||
|
DialogManager.show((setState, close) {
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text("About $appName"),
|
||||||
|
content: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: 500),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Text("Version: $version").marginSymmetric(vertical: 4.0),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString("https://rustdesk.com/privacy");
|
||||||
|
},
|
||||||
|
child: Text("Privacy Statement", style: linkStyle,).marginSymmetric(vertical: 4.0)),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString("https://rustdesk.com");
|
||||||
|
}
|
||||||
|
,child: Text("Website",style: linkStyle,).marginSymmetric(vertical: 4.0)),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Color(0xFF2c8cff)
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Copyright © 2022 Purslane Ltd.\n$license", style: TextStyle(
|
||||||
|
color: Colors.white
|
||||||
|
),),
|
||||||
|
Text("Made with heart in this chaotic world!", style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white
|
||||||
|
),)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).marginSymmetric(vertical: 4.0)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,10 @@ use crate::flutter::connection_manager::{self, get_clients_length, get_clients_s
|
|||||||
use crate::flutter::{self, Session, SESSIONS};
|
use crate::flutter::{self, Session, SESSIONS};
|
||||||
use crate::start_server;
|
use crate::start_server;
|
||||||
use crate::ui_interface;
|
use crate::ui_interface;
|
||||||
use crate::ui_interface::{change_id, get_async_job_status, get_sound_inputs, is_ok_change_id};
|
use crate::ui_interface::{
|
||||||
|
change_id, get_app_name, get_async_job_status, get_license, get_options, get_socks,
|
||||||
|
get_sound_inputs, get_version, is_ok_change_id, set_options, set_socks, test_if_valid_server,
|
||||||
|
};
|
||||||
|
|
||||||
fn initialize(app_dir: &str) {
|
fn initialize(app_dir: &str) {
|
||||||
*config::APP_DIR.write().unwrap() = app_dir.to_owned();
|
*config::APP_DIR.write().unwrap() = app_dir.to_owned();
|
||||||
@ -391,6 +394,41 @@ pub fn main_get_async_status() -> String {
|
|||||||
get_async_job_status()
|
get_async_job_status()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn main_get_options() -> String {
|
||||||
|
get_options()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main_set_options(json: String) {
|
||||||
|
let map: HashMap<String, String> = serde_json::from_str(&json).unwrap_or(HashMap::new());
|
||||||
|
if !map.is_empty() {
|
||||||
|
set_options(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main_test_if_valid_server(server: String) -> String {
|
||||||
|
test_if_valid_server(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main_set_socks(proxy: String, username: String, password: String) {
|
||||||
|
set_socks(proxy, username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main_get_socks() -> Vec<String> {
|
||||||
|
get_socks()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main_get_app_name() -> String {
|
||||||
|
get_app_name()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main_get_license() -> String {
|
||||||
|
get_license()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main_get_version() -> String {
|
||||||
|
get_version()
|
||||||
|
}
|
||||||
|
|
||||||
/// FFI for **get** commands which are idempotent.
|
/// FFI for **get** commands which are idempotent.
|
||||||
/// Return result in c string.
|
/// Return result in c string.
|
||||||
///
|
///
|
||||||
|
27
src/ui.rs
27
src/ui.rs
@ -1,20 +1,23 @@
|
|||||||
mod cm;
|
|
||||||
#[cfg(feature = "inline")]
|
|
||||||
mod inline;
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
mod macos;
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub mod win_privacy;
|
|
||||||
pub mod remote;
|
|
||||||
use crate::ui_interface::*;
|
|
||||||
use hbb_common::{allow_err, config::PeerConfig, log};
|
|
||||||
use sciter::Value;
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
iter::FromIterator,
|
iter::FromIterator,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use sciter::Value;
|
||||||
|
|
||||||
|
use hbb_common::{allow_err, config::PeerConfig, log};
|
||||||
|
|
||||||
|
use crate::ui_interface::*;
|
||||||
|
|
||||||
|
mod cm;
|
||||||
|
#[cfg(feature = "inline")]
|
||||||
|
mod inline;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod macos;
|
||||||
|
pub mod remote;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub mod win_privacy;
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
// stupid workaround for https://sciter.com/forums/topic/crash-on-latest-tis-mac-sdk-sometimes/
|
// stupid workaround for https://sciter.com/forums/topic/crash-on-latest-tis-mac-sdk-sometimes/
|
||||||
static ref STUPID_VALUES: Mutex<Vec<Arc<Vec<Value>>>> = Default::default();
|
static ref STUPID_VALUES: Mutex<Vec<Arc<Vec<Value>>>> = Default::default();
|
||||||
@ -227,7 +230,7 @@ impl UI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_options(&self) -> Value {
|
fn get_options(&self) -> Value {
|
||||||
let hashmap = get_options();
|
let hashmap: HashMap<String, String> = serde_json::from_str(&get_options()).unwrap();
|
||||||
let mut m = Value::map();
|
let mut m = Value::map();
|
||||||
for (k, v) in hashmap {
|
for (k, v) in hashmap {
|
||||||
m.set_item(k, v);
|
m.set_item(k, v);
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
use crate::common::SOFTWARE_UPDATE_URL;
|
use std::{
|
||||||
use crate::ipc;
|
collections::HashMap,
|
||||||
|
process::Child,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
time::SystemTime,
|
||||||
|
};
|
||||||
|
|
||||||
use hbb_common::{
|
use hbb_common::{
|
||||||
allow_err,
|
allow_err,
|
||||||
config::{self, Config, LocalConfig, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT},
|
config::{self, Config, LocalConfig, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT},
|
||||||
@ -11,12 +16,9 @@ use hbb_common::{
|
|||||||
tcp::FramedStream,
|
tcp::FramedStream,
|
||||||
tokio::{self, sync::mpsc, time},
|
tokio::{self, sync::mpsc, time},
|
||||||
};
|
};
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
use crate::common::SOFTWARE_UPDATE_URL;
|
||||||
process::Child,
|
use crate::ipc;
|
||||||
sync::{Arc, Mutex},
|
|
||||||
time::SystemTime,
|
|
||||||
};
|
|
||||||
|
|
||||||
type Message = RendezvousMessage;
|
type Message = RendezvousMessage;
|
||||||
|
|
||||||
@ -72,7 +74,9 @@ pub fn goto_install() {
|
|||||||
pub fn install_me(_options: String, _path: String, silent: bool, debug: bool) {
|
pub fn install_me(_options: String, _path: String, silent: bool, debug: bool) {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
allow_err!(crate::platform::windows::install_me(&_options, _path, silent, debug));
|
allow_err!(crate::platform::windows::install_me(
|
||||||
|
&_options, _path, silent, debug
|
||||||
|
));
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -185,14 +189,13 @@ pub fn using_public_server() -> bool {
|
|||||||
crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty()
|
crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_options() -> HashMap<String, String> {
|
pub fn get_options() -> String {
|
||||||
// TODO Vec<(String,String)>
|
|
||||||
let options = OPTIONS.lock().unwrap();
|
let options = OPTIONS.lock().unwrap();
|
||||||
let mut m = HashMap::new();
|
let mut m = serde_json::Map::new();
|
||||||
for (k, v) in options.iter() {
|
for (k, v) in options.iter() {
|
||||||
m.insert(k.into(), v.into());
|
m.insert(k.into(), v.to_owned().into());
|
||||||
}
|
}
|
||||||
m
|
serde_json::to_string(&m).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test_if_valid_server(host: String) -> String {
|
pub fn test_if_valid_server(host: String) -> String {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user