Merge pull request #1389 from 21pages/port-forward
port forward ui && fix TextField cursor problem
This commit is contained in:
commit
560c03e99c
@ -4,6 +4,7 @@ const double kDesktopRemoteTabBarHeight = 28.0;
|
|||||||
const String kAppTypeMain = "main";
|
const String kAppTypeMain = "main";
|
||||||
const String kAppTypeDesktopRemote = "remote";
|
const String kAppTypeDesktopRemote = "remote";
|
||||||
const String kAppTypeDesktopFileTransfer = "file transfer";
|
const String kAppTypeDesktopFileTransfer = "file transfer";
|
||||||
|
const String kAppTypeDesktopPortForward = "port forward";
|
||||||
const String kTabLabelHomePage = "Home";
|
const String kTabLabelHomePage = "Home";
|
||||||
const String kTabLabelSettingPage = "Settings";
|
const String kTabLabelSettingPage = "Settings";
|
||||||
|
|
||||||
|
@ -806,6 +806,8 @@ Future<bool> loginDialog() async {
|
|||||||
var userNameMsg = "";
|
var userNameMsg = "";
|
||||||
String pass = "";
|
String pass = "";
|
||||||
var passMsg = "";
|
var passMsg = "";
|
||||||
|
var userContontroller = TextEditingController(text: userName);
|
||||||
|
var pwdController = TextEditingController(text: pass);
|
||||||
|
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
var completer = Completer<bool>();
|
var completer = Completer<bool>();
|
||||||
@ -833,13 +835,10 @@ Future<bool> loginDialog() async {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
userName = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
errorText: userNameMsg.isNotEmpty ? userNameMsg : null),
|
errorText: userNameMsg.isNotEmpty ? userNameMsg : null),
|
||||||
controller: TextEditingController(text: userName),
|
controller: userContontroller,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -859,13 +858,10 @@ Future<bool> loginDialog() async {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
onChanged: (s) {
|
|
||||||
pass = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
errorText: passMsg.isNotEmpty ? passMsg : null),
|
errorText: passMsg.isNotEmpty ? passMsg : null),
|
||||||
controller: TextEditingController(text: pass),
|
controller: pwdController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -896,8 +892,8 @@ Future<bool> loginDialog() async {
|
|||||||
isInProgress = false;
|
isInProgress = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
userName = userName;
|
userName = userContontroller.text;
|
||||||
pass = pass;
|
pass = pwdController.text;
|
||||||
if (userName.isEmpty) {
|
if (userName.isEmpty) {
|
||||||
userNameMsg = translate("Username missed");
|
userNameMsg = translate("Username missed");
|
||||||
cancel();
|
cancel();
|
||||||
|
@ -1025,7 +1025,6 @@ class _ComboBox extends StatelessWidget {
|
|||||||
|
|
||||||
void changeServer() async {
|
void changeServer() async {
|
||||||
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
||||||
print("${oldOptions}");
|
|
||||||
String idServer = oldOptions['custom-rendezvous-server'] ?? "";
|
String idServer = oldOptions['custom-rendezvous-server'] ?? "";
|
||||||
var idServerMsg = "";
|
var idServerMsg = "";
|
||||||
String relayServer = oldOptions['relay-server'] ?? "";
|
String relayServer = oldOptions['relay-server'] ?? "";
|
||||||
@ -1033,6 +1032,10 @@ void changeServer() async {
|
|||||||
String apiServer = oldOptions['api-server'] ?? "";
|
String apiServer = oldOptions['api-server'] ?? "";
|
||||||
var apiServerMsg = "";
|
var apiServerMsg = "";
|
||||||
var key = oldOptions['key'] ?? "";
|
var key = oldOptions['key'] ?? "";
|
||||||
|
var idController = TextEditingController(text: idServer);
|
||||||
|
var relayController = TextEditingController(text: relayServer);
|
||||||
|
var apiController = TextEditingController(text: apiServer);
|
||||||
|
var keyController = TextEditingController(text: key);
|
||||||
|
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
@ -1057,13 +1060,10 @@ void changeServer() async {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
idServer = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
errorText: idServerMsg.isNotEmpty ? idServerMsg : null),
|
errorText: idServerMsg.isNotEmpty ? idServerMsg : null),
|
||||||
controller: TextEditingController(text: idServer),
|
controller: idController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1082,14 +1082,11 @@ void changeServer() async {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
relayServer = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
errorText:
|
errorText:
|
||||||
relayServerMsg.isNotEmpty ? relayServerMsg : null),
|
relayServerMsg.isNotEmpty ? relayServerMsg : null),
|
||||||
controller: TextEditingController(text: relayServer),
|
controller: relayController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1108,14 +1105,11 @@ void changeServer() async {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
apiServer = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
errorText:
|
errorText:
|
||||||
apiServerMsg.isNotEmpty ? apiServerMsg : null),
|
apiServerMsg.isNotEmpty ? apiServerMsg : null),
|
||||||
controller: TextEditingController(text: apiServer),
|
controller: apiController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1134,13 +1128,10 @@ void changeServer() async {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
key = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
controller: TextEditingController(text: key),
|
controller: keyController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1171,10 +1162,10 @@ void changeServer() async {
|
|||||||
isInProgress = false;
|
isInProgress = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
idServer = idServer.trim();
|
idServer = idController.text.trim();
|
||||||
relayServer = relayServer.trim();
|
relayServer = relayController.text.trim();
|
||||||
apiServer = apiServer.trim();
|
apiServer = apiController.text.trim().toLowerCase();
|
||||||
key = key.trim();
|
key = keyController.text.trim();
|
||||||
|
|
||||||
if (idServer.isNotEmpty) {
|
if (idServer.isNotEmpty) {
|
||||||
idServerMsg = translate(
|
idServerMsg = translate(
|
||||||
@ -1230,6 +1221,7 @@ void changeWhiteList() async {
|
|||||||
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
||||||
var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(',');
|
var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(',');
|
||||||
var newWhiteListField = newWhiteList.join('\n');
|
var newWhiteListField = newWhiteList.join('\n');
|
||||||
|
var controller = TextEditingController(text: newWhiteListField);
|
||||||
var msg = "";
|
var msg = "";
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
@ -1246,15 +1238,12 @@ void changeWhiteList() async {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
newWhiteListField = s;
|
|
||||||
},
|
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
errorText: msg.isEmpty ? null : translate(msg),
|
errorText: msg.isEmpty ? null : translate(msg),
|
||||||
),
|
),
|
||||||
controller: TextEditingController(text: newWhiteListField),
|
controller: controller,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1277,7 +1266,7 @@ void changeWhiteList() async {
|
|||||||
msg = "";
|
msg = "";
|
||||||
isInProgress = true;
|
isInProgress = true;
|
||||||
});
|
});
|
||||||
newWhiteListField = newWhiteListField.trim();
|
newWhiteListField = controller.text.trim();
|
||||||
var newWhiteList = "";
|
var newWhiteList = "";
|
||||||
if (newWhiteListField.isEmpty) {
|
if (newWhiteListField.isEmpty) {
|
||||||
// pass
|
// pass
|
||||||
@ -1319,6 +1308,9 @@ void changeSocks5Proxy() async {
|
|||||||
username = socks[1];
|
username = socks[1];
|
||||||
password = socks[2];
|
password = socks[2];
|
||||||
}
|
}
|
||||||
|
var proxyController = TextEditingController(text: proxy);
|
||||||
|
var userController = TextEditingController(text: username);
|
||||||
|
var pwdController = TextEditingController(text: password);
|
||||||
|
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
@ -1343,13 +1335,10 @@ void changeSocks5Proxy() async {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
proxy = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
|
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
|
||||||
controller: TextEditingController(text: proxy),
|
controller: proxyController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1368,13 +1357,10 @@ void changeSocks5Proxy() async {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
username = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
controller: TextEditingController(text: username),
|
controller: userController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1393,13 +1379,10 @@ void changeSocks5Proxy() async {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
password = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
controller: TextEditingController(text: password),
|
controller: pwdController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1428,9 +1411,9 @@ void changeSocks5Proxy() async {
|
|||||||
isInProgress = false;
|
isInProgress = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
proxy = proxy.trim();
|
proxy = proxyController.text.trim();
|
||||||
username = username.trim();
|
username = userController.text.trim();
|
||||||
password = password.trim();
|
password = pwdController.text.trim();
|
||||||
|
|
||||||
if (proxy.isNotEmpty) {
|
if (proxy.isNotEmpty) {
|
||||||
proxyMsg =
|
proxyMsg =
|
||||||
|
348
flutter/lib/desktop/pages/port_forward_page.dart
Normal file
348
flutter/lib/desktop/pages/port_forward_page.dart
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:wakelock/wakelock.dart';
|
||||||
|
|
||||||
|
const double _kColumn1Width = 30;
|
||||||
|
const double _kColumn4Width = 100;
|
||||||
|
const double _kRowHeight = 50;
|
||||||
|
const double _kTextLeftMargin = 20;
|
||||||
|
|
||||||
|
class _PortForward {
|
||||||
|
int localPort;
|
||||||
|
String remoteHost;
|
||||||
|
int remotePort;
|
||||||
|
|
||||||
|
_PortForward.fromJson(List<dynamic> json)
|
||||||
|
: localPort = json[0] as int,
|
||||||
|
remoteHost = json[1] as String,
|
||||||
|
remotePort = json[2] as int;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PortForwardPage extends StatefulWidget {
|
||||||
|
const PortForwardPage({Key? key, required this.id, required this.isRDP})
|
||||||
|
: super(key: key);
|
||||||
|
final String id;
|
||||||
|
final bool isRDP;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PortForwardPage> createState() => _PortForwardPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PortForwardPageState extends State<PortForwardPage>
|
||||||
|
with AutomaticKeepAliveClientMixin {
|
||||||
|
final bool isRdp = false;
|
||||||
|
final TextEditingController localPortController = TextEditingController();
|
||||||
|
final TextEditingController remoteHostController = TextEditingController();
|
||||||
|
final TextEditingController remotePortController = TextEditingController();
|
||||||
|
RxList<_PortForward> pfs = RxList.empty(growable: true);
|
||||||
|
late FFI _ffi;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_ffi = FFI();
|
||||||
|
// _ffi.connect(widget.id, isPortForward: true);
|
||||||
|
Get.put(_ffi, tag: 'pf_${widget.id}');
|
||||||
|
if (!Platform.isLinux) {
|
||||||
|
Wakelock.enable();
|
||||||
|
}
|
||||||
|
print("init success with id ${widget.id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_ffi.close();
|
||||||
|
_ffi.dialogManager.dismissAll();
|
||||||
|
if (!Platform.isLinux) {
|
||||||
|
Wakelock.disable();
|
||||||
|
}
|
||||||
|
Get.delete<FFI>(tag: 'pf_${widget.id}');
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: MyTheme.color(context).grayBg,
|
||||||
|
body: FutureBuilder(future: () async {
|
||||||
|
if (!isRdp) {
|
||||||
|
refreshTunnelConfig();
|
||||||
|
}
|
||||||
|
}(), builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
width: 20, color: MyTheme.color(context).grayBg!)),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
buildPrompt(context),
|
||||||
|
Flexible(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: MyTheme.color(context).bg,
|
||||||
|
border: Border.all(width: 1, color: MyTheme.border)),
|
||||||
|
child:
|
||||||
|
widget.isRDP ? buildRdp(context) : buildTunnel(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const Offstage();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPrompt(BuildContext context) {
|
||||||
|
return Obx(() => Offstage(
|
||||||
|
offstage: pfs.isEmpty && !widget.isRDP,
|
||||||
|
child: Container(
|
||||||
|
height: 45,
|
||||||
|
color: const Color(0xFF007F00),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
translate('Listening ...'),
|
||||||
|
style: const TextStyle(fontSize: 16, color: Colors.white),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
translate('not_close_tcp_tip'),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 10, color: Color(0xFFDDDDDD), height: 1.2),
|
||||||
|
)
|
||||||
|
])).marginOnly(bottom: 8),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTunnel(BuildContext context) {
|
||||||
|
text(String lable) => Expanded(
|
||||||
|
child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin));
|
||||||
|
|
||||||
|
return Theme(
|
||||||
|
data: Theme.of(context)
|
||||||
|
.copyWith(backgroundColor: MyTheme.color(context).bg),
|
||||||
|
child: Obx(() => ListView.builder(
|
||||||
|
itemCount: pfs.length + 2,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return Container(
|
||||||
|
height: 25,
|
||||||
|
color: MyTheme.color(context).grayBg,
|
||||||
|
child: Row(children: [
|
||||||
|
text('Local Port'),
|
||||||
|
const SizedBox(width: _kColumn1Width),
|
||||||
|
text('Remote Host'),
|
||||||
|
text('Remote Port'),
|
||||||
|
SizedBox(
|
||||||
|
width: _kColumn4Width, child: Text(translate('Action')))
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
} else if (index == 1) {
|
||||||
|
return buildTunnelAddRow(context);
|
||||||
|
} else {
|
||||||
|
return buildTunnelDataRow(context, pfs[index - 2], index - 2);
|
||||||
|
}
|
||||||
|
}))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTunnelAddRow(BuildContext context) {
|
||||||
|
var portInputFormatter = [
|
||||||
|
FilteringTextInputFormatter.allow(RegExp(
|
||||||
|
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])$'))
|
||||||
|
];
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
height: _kRowHeight,
|
||||||
|
decoration: BoxDecoration(color: MyTheme.color(context).bg),
|
||||||
|
child: Row(children: [
|
||||||
|
buildTunnelInputCell(context,
|
||||||
|
controller: localPortController,
|
||||||
|
inputFormatters: portInputFormatter),
|
||||||
|
const SizedBox(
|
||||||
|
width: _kColumn1Width, child: Icon(Icons.arrow_forward_sharp)),
|
||||||
|
buildTunnelInputCell(context,
|
||||||
|
controller: remoteHostController, hint: 'localhost'),
|
||||||
|
buildTunnelInputCell(context,
|
||||||
|
controller: remotePortController,
|
||||||
|
inputFormatters: portInputFormatter),
|
||||||
|
SizedBox(
|
||||||
|
width: _kColumn4Width,
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
elevation: 0, side: const BorderSide(color: MyTheme.border)),
|
||||||
|
onPressed: () async {
|
||||||
|
int? localPort = int.tryParse(localPortController.text);
|
||||||
|
int? remotePort = int.tryParse(remotePortController.text);
|
||||||
|
if (localPort != null &&
|
||||||
|
remotePort != null &&
|
||||||
|
(remoteHostController.text.isEmpty ||
|
||||||
|
remoteHostController.text.trim().isNotEmpty)) {
|
||||||
|
await bind.mainAddPortForward(
|
||||||
|
id: widget.id,
|
||||||
|
localPort: localPort,
|
||||||
|
remoteHost: remoteHostController.text.trim().isEmpty
|
||||||
|
? 'localhost'
|
||||||
|
: remoteHostController.text.trim(),
|
||||||
|
remotePort: remotePort);
|
||||||
|
localPortController.clear();
|
||||||
|
remoteHostController.clear();
|
||||||
|
remotePortController.clear();
|
||||||
|
refreshTunnelConfig();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
translate('Add'),
|
||||||
|
),
|
||||||
|
).marginAll(10),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTunnelInputCell(BuildContext context,
|
||||||
|
{required TextEditingController controller,
|
||||||
|
List<TextInputFormatter>? inputFormatters,
|
||||||
|
String? hint}) {
|
||||||
|
return Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
inputFormatters: inputFormatters,
|
||||||
|
cursorColor: MyTheme.color(context).text,
|
||||||
|
cursorHeight: 20,
|
||||||
|
cursorWidth: 1,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: MyTheme.color(context).border!)),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: MyTheme.color(context).border!)),
|
||||||
|
fillColor: MyTheme.color(context).bg,
|
||||||
|
contentPadding: const EdgeInsets.all(10),
|
||||||
|
hintText: hint,
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: MyTheme.color(context).placeholder, fontSize: 16)),
|
||||||
|
style: TextStyle(color: MyTheme.color(context).text, fontSize: 16),
|
||||||
|
).marginAll(10),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTunnelDataRow(BuildContext context, _PortForward pf, int index) {
|
||||||
|
text(String lable) => Expanded(
|
||||||
|
child: Text(lable, style: const TextStyle(fontSize: 20))
|
||||||
|
.marginOnly(left: _kTextLeftMargin));
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
height: _kRowHeight,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: index % 2 == 0
|
||||||
|
? isDarkTheme()
|
||||||
|
? const Color(0xFF202020)
|
||||||
|
: const Color(0xFFF4F5F6)
|
||||||
|
: MyTheme.color(context).bg),
|
||||||
|
child: Row(children: [
|
||||||
|
text(pf.localPort.toString()),
|
||||||
|
const SizedBox(width: _kColumn1Width),
|
||||||
|
text(pf.remoteHost),
|
||||||
|
text(pf.remotePort.toString()),
|
||||||
|
SizedBox(
|
||||||
|
width: _kColumn4Width,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () async {
|
||||||
|
await bind.mainRemovePortForward(
|
||||||
|
id: widget.id, localPort: pf.localPort);
|
||||||
|
refreshTunnelConfig();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshTunnelConfig() async {
|
||||||
|
String peer = await bind.mainGetPeer(id: widget.id);
|
||||||
|
Map<String, dynamic> config = jsonDecode(peer);
|
||||||
|
List<dynamic> infos = config['port_forwards'] as List;
|
||||||
|
List<_PortForward> result = List.empty(growable: true);
|
||||||
|
for (var e in infos) {
|
||||||
|
result.add(_PortForward.fromJson(e));
|
||||||
|
}
|
||||||
|
pfs.value = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildRdp(BuildContext context) {
|
||||||
|
text1(String lable) =>
|
||||||
|
Expanded(child: Text(lable).marginOnly(left: _kTextLeftMargin));
|
||||||
|
text2(String lable) => Expanded(
|
||||||
|
child: Text(
|
||||||
|
lable,
|
||||||
|
style: TextStyle(fontSize: 20),
|
||||||
|
).marginOnly(left: _kTextLeftMargin));
|
||||||
|
return Theme(
|
||||||
|
data: Theme.of(context)
|
||||||
|
.copyWith(backgroundColor: MyTheme.color(context).bg),
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: 2,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return Container(
|
||||||
|
height: 25,
|
||||||
|
color: MyTheme.color(context).grayBg,
|
||||||
|
child: Row(children: [
|
||||||
|
text1('Local Port'),
|
||||||
|
const SizedBox(width: _kColumn1Width),
|
||||||
|
text1('Remote Host'),
|
||||||
|
text1('Remote Port'),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(
|
||||||
|
height: _kRowHeight,
|
||||||
|
decoration: BoxDecoration(color: MyTheme.color(context).bg),
|
||||||
|
child: Row(children: [
|
||||||
|
Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 120,
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
elevation: 0,
|
||||||
|
side: const BorderSide(color: MyTheme.border)),
|
||||||
|
onPressed: () {},
|
||||||
|
child: Text(
|
||||||
|
translate('New RDP'),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w300, fontSize: 14),
|
||||||
|
),
|
||||||
|
).marginSymmetric(vertical: 10),
|
||||||
|
).marginOnly(left: 20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: _kColumn1Width,
|
||||||
|
child: Icon(Icons.arrow_forward_sharp)),
|
||||||
|
text2('localhost'),
|
||||||
|
text2('RDP'),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
}
|
106
flutter/lib/desktop/pages/port_forward_tab_page.dart
Normal file
106
flutter/lib/desktop/pages/port_forward_tab_page.dart
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/port_forward_page.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class PortForwardTabPage extends StatefulWidget {
|
||||||
|
final Map<String, dynamic> params;
|
||||||
|
|
||||||
|
const PortForwardTabPage({Key? key, required this.params}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PortForwardTabPage> createState() => _PortForwardTabPageState(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||||
|
final tabController = Get.put(DesktopTabController());
|
||||||
|
|
||||||
|
static final IconData selectedIcon = Icons.forward_sharp;
|
||||||
|
static final IconData unselectedIcon = Icons.forward_outlined;
|
||||||
|
|
||||||
|
_PortForwardTabPageState(Map<String, dynamic> params) {
|
||||||
|
tabController.add(TabInfo(
|
||||||
|
key: params['id'] + params['isRDP'].toString(),
|
||||||
|
label: params['id'],
|
||||||
|
selectedIcon: selectedIcon,
|
||||||
|
unselectedIcon: unselectedIcon,
|
||||||
|
page: PortForwardPage(
|
||||||
|
key: ValueKey(params['id']),
|
||||||
|
id: params['id'],
|
||||||
|
isRDP: params['isRDP'],
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
tabController.onRemove = (_, id) => onRemoveId(id);
|
||||||
|
|
||||||
|
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||||
|
print(
|
||||||
|
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
|
||||||
|
// for simplify, just replace connectionId
|
||||||
|
if (call.method == "new_port_forward") {
|
||||||
|
final args = jsonDecode(call.arguments);
|
||||||
|
final id = args['id'];
|
||||||
|
final isRDP = args['isRDP'];
|
||||||
|
window_on_top(windowId());
|
||||||
|
tabController.add(TabInfo(
|
||||||
|
key: id,
|
||||||
|
label: id,
|
||||||
|
selectedIcon: selectedIcon,
|
||||||
|
unselectedIcon: unselectedIcon,
|
||||||
|
page: PortForwardPage(id: id, isRDP: isRDP)));
|
||||||
|
} else if (call.method == "onDestroy") {
|
||||||
|
tabController.state.value.tabs.forEach((tab) {
|
||||||
|
print("executing onDestroy hook, closing ${tab.label}}");
|
||||||
|
final tag = tab.label;
|
||||||
|
ffi(tag).close().then((_) {
|
||||||
|
Get.delete<FFI>(tag: tag);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Get.back();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
|
||||||
|
return SubWindowDragToResizeArea(
|
||||||
|
windowId: windowId(),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: MyTheme.color(context).border!)),
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: MyTheme.color(context).bg,
|
||||||
|
body: DesktopTab(
|
||||||
|
controller: tabController,
|
||||||
|
theme: theme,
|
||||||
|
isMainWindow: false,
|
||||||
|
tail: AddButton(
|
||||||
|
theme: theme,
|
||||||
|
).paddingOnly(left: 10),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onRemoveId(String id) {
|
||||||
|
ffi("pf_$id").close();
|
||||||
|
if (tabController.state.value.tabs.length == 0) {
|
||||||
|
WindowController.fromWindowId(windowId()).close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int windowId() {
|
||||||
|
return widget.params["windowId"];
|
||||||
|
}
|
||||||
|
}
|
26
flutter/lib/desktop/screen/desktop_port_forward_screen.dart
Normal file
26
flutter/lib/desktop/screen/desktop_port_forward_screen.dart
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/port_forward_tab_page.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
/// multi-tab file port forward screen
|
||||||
|
class DesktopPortForwardScreen extends StatelessWidget {
|
||||||
|
final Map<String, dynamic> params;
|
||||||
|
|
||||||
|
const DesktopPortForwardScreen({Key? key, required this.params})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
||||||
|
],
|
||||||
|
child: Scaffold(
|
||||||
|
body: PortForwardTabPage(
|
||||||
|
params: params,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:contextmenu/contextmenu.dart';
|
import 'package:contextmenu/contextmenu.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
@ -284,11 +285,20 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
|
|
||||||
/// Connect to a peer with [id].
|
/// Connect to a peer with [id].
|
||||||
/// If [isFileTransfer], starts a session only for file transfer.
|
/// If [isFileTransfer], starts a session only for file transfer.
|
||||||
void _connect(String id, {bool isFileTransfer = false}) async {
|
/// If [isTcpTunneling], starts a session only for tcp tunneling.
|
||||||
|
/// If [isRDP], starts a session only for rdp.
|
||||||
|
void _connect(String id,
|
||||||
|
{bool isFileTransfer = false,
|
||||||
|
bool isTcpTunneling = false,
|
||||||
|
bool isRDP = false}) async {
|
||||||
if (id == '') return;
|
if (id == '') return;
|
||||||
id = id.replaceAll(' ', '');
|
id = id.replaceAll(' ', '');
|
||||||
|
assert(!(isFileTransfer && isTcpTunneling && isRDP),
|
||||||
|
"more than one connect type");
|
||||||
if (isFileTransfer) {
|
if (isFileTransfer) {
|
||||||
await rustDeskWinManager.new_file_transfer(id);
|
await rustDeskWinManager.new_file_transfer(id);
|
||||||
|
} else if (isTcpTunneling || isRDP) {
|
||||||
|
await rustDeskWinManager.new_port_forward(id, isRDP);
|
||||||
} else {
|
} else {
|
||||||
await rustDeskWinManager.new_remote_desktop(id);
|
await rustDeskWinManager.new_remote_desktop(id);
|
||||||
}
|
}
|
||||||
@ -307,12 +317,18 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
items: await super.widget.popupMenuItemsFunc(),
|
items: await super.widget.popupMenuItemsFunc(),
|
||||||
elevation: 8,
|
elevation: 8,
|
||||||
);
|
);
|
||||||
if (value == 'remove') {
|
if (value == 'connect') {
|
||||||
|
_connect(id);
|
||||||
|
} else if (value == 'file') {
|
||||||
|
_connect(id, isFileTransfer: true);
|
||||||
|
} else if (value == 'tcp-tunnel') {
|
||||||
|
_connect(id, isTcpTunneling: true);
|
||||||
|
} else if (value == 'RDP') {
|
||||||
|
_connect(id, isRDP: true);
|
||||||
|
} else if (value == 'remove') {
|
||||||
await bind.mainRemovePeer(id: id);
|
await bind.mainRemovePeer(id: id);
|
||||||
removePreference(id);
|
removePreference(id);
|
||||||
Get.forceAppUpdate(); // TODO use inner model / state
|
Get.forceAppUpdate(); // TODO use inner model / state
|
||||||
} else if (value == 'file') {
|
|
||||||
_connect(id, isFileTransfer: true);
|
|
||||||
} else if (value == 'add-fav') {
|
} else if (value == 'add-fav') {
|
||||||
final favs = (await bind.mainGetFav()).toList();
|
final favs = (await bind.mainGetFav()).toList();
|
||||||
if (favs.indexOf(id) < 0) {
|
if (favs.indexOf(id) < 0) {
|
||||||
@ -325,8 +341,6 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
bind.mainStoreFav(favs: favs);
|
bind.mainStoreFav(favs: favs);
|
||||||
Get.forceAppUpdate(); // TODO use inner model / state
|
Get.forceAppUpdate(); // TODO use inner model / state
|
||||||
}
|
}
|
||||||
} else if (value == 'connect') {
|
|
||||||
_connect(id, isFileTransfer: false);
|
|
||||||
} else if (value == 'ab-delete') {
|
} else if (value == 'ab-delete') {
|
||||||
gFFI.abModel.deletePeer(id);
|
gFFI.abModel.deletePeer(id);
|
||||||
await gFFI.abModel.updateAb();
|
await gFFI.abModel.updateAb();
|
||||||
@ -448,6 +462,7 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
void _rename(String id) async {
|
void _rename(String id) async {
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
var name = await bind.mainGetPeerOption(id: id, key: 'alias');
|
var name = await bind.mainGetPeerOption(id: id, key: 'alias');
|
||||||
|
var controller = TextEditingController(text: name);
|
||||||
if (widget.type == PeerType.ab) {
|
if (widget.type == PeerType.ab) {
|
||||||
final peer = gFFI.abModel.peers.firstWhere((p) => id == p['id']);
|
final peer = gFFI.abModel.peers.firstWhere((p) => id == p['id']);
|
||||||
if (peer == null) {
|
if (peer == null) {
|
||||||
@ -456,7 +471,6 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
name = peer['alias'] ?? "";
|
name = peer['alias'] ?? "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final k = GlobalKey<FormState>();
|
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate("Rename")),
|
title: Text(translate("Rename")),
|
||||||
@ -466,22 +480,9 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: k,
|
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: TextEditingController(text: name),
|
controller: controller,
|
||||||
decoration: InputDecoration(border: OutlineInputBorder()),
|
decoration: InputDecoration(border: OutlineInputBorder()),
|
||||||
onChanged: (newStr) {
|
|
||||||
name = newStr;
|
|
||||||
},
|
|
||||||
validator: (s) {
|
|
||||||
if (s == null || s.isEmpty) {
|
|
||||||
return translate("Empty");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onSaved: (s) {
|
|
||||||
name = s ?? "unnamed";
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -499,22 +500,17 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
setState(() {
|
setState(() {
|
||||||
isInProgress = true;
|
isInProgress = true;
|
||||||
});
|
});
|
||||||
if (k.currentState != null) {
|
name = controller.text;
|
||||||
if (k.currentState!.validate()) {
|
await bind.mainSetPeerOption(id: id, key: 'alias', value: name);
|
||||||
k.currentState!.save();
|
if (widget.type == PeerType.ab) {
|
||||||
await bind.mainSetPeerOption(
|
gFFI.abModel.setPeerOption(id, 'alias', name);
|
||||||
id: id, key: 'alias', value: name);
|
await gFFI.abModel.updateAb();
|
||||||
if (widget.type == PeerType.ab) {
|
} else {
|
||||||
gFFI.abModel.setPeerOption(id, 'alias', name);
|
Future.delayed(Duration.zero, () {
|
||||||
await gFFI.abModel.updateAb();
|
this.setState(() {});
|
||||||
} else {
|
});
|
||||||
Future.delayed(Duration.zero, () {
|
|
||||||
this.setState(() {});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
close();
|
||||||
setState(() {
|
setState(() {
|
||||||
isInProgress = false;
|
isInProgress = false;
|
||||||
});
|
});
|
||||||
@ -554,7 +550,7 @@ class RecentPeerCard extends BasePeerCard {
|
|||||||
: super(peer: peer, key: key, type: PeerType.recent);
|
: super(peer: peer, key: key, type: PeerType.recent);
|
||||||
|
|
||||||
Future<List<PopupMenuItem<String>>> _getPopupMenuItems() async {
|
Future<List<PopupMenuItem<String>>> _getPopupMenuItems() async {
|
||||||
return [
|
var items = [
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
child: Text(translate('Connect')), value: 'connect'),
|
child: Text(translate('Connect')), value: 'connect'),
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
@ -570,6 +566,10 @@ class RecentPeerCard extends BasePeerCard {
|
|||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
child: Text(translate('Add to Favorites')), value: 'add-fav'),
|
child: Text(translate('Add to Favorites')), value: 'add-fav'),
|
||||||
];
|
];
|
||||||
|
if (peer.platform == 'Windows') {
|
||||||
|
items.insert(3, _rdpMenuItem(peer.id));
|
||||||
|
}
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -578,7 +578,7 @@ class FavoritePeerCard extends BasePeerCard {
|
|||||||
: super(peer: peer, key: key, type: PeerType.fav);
|
: super(peer: peer, key: key, type: PeerType.fav);
|
||||||
|
|
||||||
Future<List<PopupMenuItem<String>>> _getPopupMenuItems() async {
|
Future<List<PopupMenuItem<String>>> _getPopupMenuItems() async {
|
||||||
return [
|
var items = [
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
child: Text(translate('Connect')), value: 'connect'),
|
child: Text(translate('Connect')), value: 'connect'),
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
@ -594,6 +594,10 @@ class FavoritePeerCard extends BasePeerCard {
|
|||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
child: Text(translate('Remove from Favorites')), value: 'remove-fav'),
|
child: Text(translate('Remove from Favorites')), value: 'remove-fav'),
|
||||||
];
|
];
|
||||||
|
if (peer.platform == 'Windows') {
|
||||||
|
items.insert(3, _rdpMenuItem(peer.id));
|
||||||
|
}
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -602,7 +606,7 @@ class DiscoveredPeerCard extends BasePeerCard {
|
|||||||
: super(peer: peer, key: key, type: PeerType.discovered);
|
: super(peer: peer, key: key, type: PeerType.discovered);
|
||||||
|
|
||||||
Future<List<PopupMenuItem<String>>> _getPopupMenuItems() async {
|
Future<List<PopupMenuItem<String>>> _getPopupMenuItems() async {
|
||||||
return [
|
var items = [
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
child: Text(translate('Connect')), value: 'connect'),
|
child: Text(translate('Connect')), value: 'connect'),
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
@ -618,6 +622,10 @@ class DiscoveredPeerCard extends BasePeerCard {
|
|||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
child: Text(translate('Add to Favorites')), value: 'add-fav'),
|
child: Text(translate('Add to Favorites')), value: 'add-fav'),
|
||||||
];
|
];
|
||||||
|
if (peer.platform == 'Windows') {
|
||||||
|
items.insert(3, _rdpMenuItem(peer.id));
|
||||||
|
}
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -626,7 +634,7 @@ class AddressBookPeerCard extends BasePeerCard {
|
|||||||
: super(peer: peer, key: key, type: PeerType.ab);
|
: super(peer: peer, key: key, type: PeerType.ab);
|
||||||
|
|
||||||
Future<List<PopupMenuItem<String>>> _getPopupMenuItems() async {
|
Future<List<PopupMenuItem<String>>> _getPopupMenuItems() async {
|
||||||
return [
|
var items = [
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
child: Text(translate('Connect')), value: 'connect'),
|
child: Text(translate('Connect')), value: 'connect'),
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
@ -645,6 +653,10 @@ class AddressBookPeerCard extends BasePeerCard {
|
|||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
child: Text(translate('Edit Tag')), value: 'ab-edit-tag'),
|
child: Text(translate('Edit Tag')), value: 'ab-edit-tag'),
|
||||||
];
|
];
|
||||||
|
if (peer.platform == 'Windows') {
|
||||||
|
items.insert(3, _rdpMenuItem(peer.id));
|
||||||
|
}
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -664,3 +676,136 @@ Future<PopupMenuItem<String>> _forceAlwaysRelayMenuItem(String id) async {
|
|||||||
),
|
),
|
||||||
value: 'force-always-relay');
|
value: 'force-always-relay');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PopupMenuItem<String> _rdpMenuItem(String id) {
|
||||||
|
return PopupMenuItem<String>(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text('RDP'),
|
||||||
|
SizedBox(width: 20),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.edit),
|
||||||
|
onPressed: () => _rdpDialog(id),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
value: 'RDP');
|
||||||
|
}
|
||||||
|
|
||||||
|
void _rdpDialog(String id) async {
|
||||||
|
final portController = TextEditingController(
|
||||||
|
text: await bind.mainGetPeerOption(id: id, key: 'rdp_port'));
|
||||||
|
final userController = TextEditingController(
|
||||||
|
text: await bind.mainGetPeerOption(id: id, key: 'rdp_username'));
|
||||||
|
final passwordContorller = TextEditingController(
|
||||||
|
text: await bind.mainGetPeerOption(id: id, key: 'rdp_password'));
|
||||||
|
RxBool secure = true.obs;
|
||||||
|
|
||||||
|
gFFI.dialogManager.show((setState, close) {
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text('RDP ' + translate('Settings')),
|
||||||
|
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('Port')}:",
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
).marginOnly(bottom: 16.0)),
|
||||||
|
SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.allow(RegExp(
|
||||||
|
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])$'))
|
||||||
|
],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(), hintText: '3389'),
|
||||||
|
controller: portController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: 100),
|
||||||
|
child: Text(
|
||||||
|
"${translate('Username')}:",
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
).marginOnly(bottom: 16.0)),
|
||||||
|
SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(border: OutlineInputBorder()),
|
||||||
|
controller: userController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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: Obx(() => TextField(
|
||||||
|
obscureText: secure.value,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
onPressed: () => secure.value = !secure.value,
|
||||||
|
icon: Icon(secure.value
|
||||||
|
? Icons.visibility_off
|
||||||
|
: Icons.visibility))),
|
||||||
|
controller: passwordContorller,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
child: Text(translate("Cancel"))),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await bind.mainSetPeerOption(
|
||||||
|
id: id, key: 'rdp_port', value: portController.text.trim());
|
||||||
|
await bind.mainSetPeerOption(
|
||||||
|
id: id, key: 'rdp_username', value: userController.text);
|
||||||
|
await bind.mainSetPeerOption(
|
||||||
|
id: id, key: 'rdp_password', value: passwordContorller.text);
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/server_page.dart';
|
import 'package:flutter_hbb/desktop/pages/server_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
|
import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart';
|
||||||
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
|
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@ -47,6 +48,9 @@ Future<Null> main(List<String> args) async {
|
|||||||
case WindowType.FileTransfer:
|
case WindowType.FileTransfer:
|
||||||
runFileTransferScreen(argument);
|
runFileTransferScreen(argument);
|
||||||
break;
|
break;
|
||||||
|
case WindowType.PortForward:
|
||||||
|
runPortForwardScreen(argument);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -133,6 +137,23 @@ void runFileTransferScreen(Map<String, dynamic> argument) async {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void runPortForwardScreen(Map<String, dynamic> argument) async {
|
||||||
|
await initEnv(kAppTypeDesktopPortForward);
|
||||||
|
runApp(
|
||||||
|
GetMaterialApp(
|
||||||
|
navigatorKey: globalKey,
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
title: 'RustDesk - Port Forward',
|
||||||
|
theme: getCurrentTheme(),
|
||||||
|
home: DesktopPortForwardScreen(params: argument),
|
||||||
|
navigatorObservers: [
|
||||||
|
// FirebaseAnalyticsObserver(analytics: analytics),
|
||||||
|
],
|
||||||
|
builder: _keepScaleBuilder(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void runConnectionManagerScreen() async {
|
void runConnectionManagerScreen() async {
|
||||||
// initialize window
|
// initialize window
|
||||||
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(300, 400));
|
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(300, 400));
|
||||||
@ -190,8 +211,13 @@ class App extends StatelessWidget {
|
|||||||
// FirebaseAnalyticsObserver(analytics: analytics),
|
// FirebaseAnalyticsObserver(analytics: analytics),
|
||||||
],
|
],
|
||||||
builder: isAndroid
|
builder: isAndroid
|
||||||
? (_, child) => AccessibilityListener(
|
? (context, child) => AccessibilityListener(
|
||||||
child: child,
|
child: MediaQuery(
|
||||||
|
data: MediaQuery.of(context).copyWith(
|
||||||
|
textScaleFactor: 1.0,
|
||||||
|
),
|
||||||
|
child: child ?? Container(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: _keepScaleBuilder(),
|
: _keepScaleBuilder(),
|
||||||
),
|
),
|
||||||
|
@ -1051,17 +1051,24 @@ class FFI {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect with the given [id]. Only transfer file if [isFileTransfer].
|
/// Connect with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward].
|
||||||
void connect(String id,
|
void connect(String id,
|
||||||
{bool isFileTransfer = false, double tabBarHeight = 0.0}) {
|
{bool isFileTransfer = false,
|
||||||
if (!isFileTransfer) {
|
bool isPortForward = false,
|
||||||
|
double tabBarHeight = 0.0}) {
|
||||||
|
assert(!(isFileTransfer && isPortForward), "more than one connect type");
|
||||||
|
if (isFileTransfer) {
|
||||||
|
id = 'ft_${id}';
|
||||||
|
} else if (isPortForward) {
|
||||||
|
id = 'pf_${id}';
|
||||||
|
} else {
|
||||||
chatModel.resetClientMode();
|
chatModel.resetClientMode();
|
||||||
canvasModel.id = id;
|
canvasModel.id = id;
|
||||||
imageModel._id = id;
|
imageModel._id = id;
|
||||||
cursorModel.id = id;
|
cursorModel.id = id;
|
||||||
}
|
}
|
||||||
id = isFileTransfer ? 'ft_${id}' : id;
|
final stream = bind.sessionConnect(
|
||||||
final stream = bind.sessionConnect(id: id, isFileTransfer: isFileTransfer);
|
id: id, isFileTransfer: isFileTransfer, isPortForward: isPortForward);
|
||||||
final cb = ffiModel.startEventListener(id);
|
final cb = ffiModel.startEventListener(id);
|
||||||
() async {
|
() async {
|
||||||
await for (final message in stream) {
|
await for (final message in stream) {
|
||||||
|
@ -35,6 +35,7 @@ class RustDeskMultiWindowManager {
|
|||||||
|
|
||||||
int? _remoteDesktopWindowId;
|
int? _remoteDesktopWindowId;
|
||||||
int? _fileTransferWindowId;
|
int? _fileTransferWindowId;
|
||||||
|
int? _portForwardWindowId;
|
||||||
|
|
||||||
Future<dynamic> new_remote_desktop(String remote_id) async {
|
Future<dynamic> new_remote_desktop(String remote_id) async {
|
||||||
final msg =
|
final msg =
|
||||||
@ -87,6 +88,34 @@ class RustDeskMultiWindowManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<dynamic> new_port_forward(String remote_id, bool isRDP) async {
|
||||||
|
final msg = jsonEncode({
|
||||||
|
"type": WindowType.PortForward.index,
|
||||||
|
"id": remote_id,
|
||||||
|
"isRDP": isRDP
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final ids = await DesktopMultiWindow.getAllSubWindowIds();
|
||||||
|
if (!ids.contains(_portForwardWindowId)) {
|
||||||
|
_portForwardWindowId = null;
|
||||||
|
}
|
||||||
|
} on Error {
|
||||||
|
_portForwardWindowId = null;
|
||||||
|
}
|
||||||
|
if (_portForwardWindowId == null) {
|
||||||
|
final portForwardController = await DesktopMultiWindow.createWindow(msg);
|
||||||
|
portForwardController
|
||||||
|
..setFrame(const Offset(0, 0) & const Size(1280, 720))
|
||||||
|
..center()
|
||||||
|
..setTitle("rustdesk - port forward")
|
||||||
|
..show();
|
||||||
|
_portForwardWindowId = portForwardController.windowId;
|
||||||
|
} else {
|
||||||
|
return call(WindowType.PortForward, "new_port_forward", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<dynamic> call(WindowType type, String methodName, dynamic args) async {
|
Future<dynamic> call(WindowType type, String methodName, dynamic args) async {
|
||||||
int? windowId = findWindowByType(type);
|
int? windowId = findWindowByType(type);
|
||||||
if (windowId == null) {
|
if (windowId == null) {
|
||||||
@ -104,7 +133,7 @@ class RustDeskMultiWindowManager {
|
|||||||
case WindowType.FileTransfer:
|
case WindowType.FileTransfer:
|
||||||
return _fileTransferWindowId;
|
return _fileTransferWindowId;
|
||||||
case WindowType.PortForward:
|
case WindowType.PortForward:
|
||||||
break;
|
return _portForwardWindowId;
|
||||||
case WindowType.Unknown:
|
case WindowType.Unknown:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -120,7 +149,7 @@ class RustDeskMultiWindowManager {
|
|||||||
await Future.wait(WindowType.values.map((e) => closeWindows(e)));
|
await Future.wait(WindowType.values.map((e) => closeWindows(e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> closeWindows(WindowType type) async {
|
Future<void> closeWindows(WindowType type) async {
|
||||||
if (type == WindowType.Main) {
|
if (type == WindowType.Main) {
|
||||||
// skip main window, use window manager instead
|
// skip main window, use window manager instead
|
||||||
return;
|
return;
|
||||||
|
@ -70,7 +70,13 @@ impl Session {
|
|||||||
///
|
///
|
||||||
/// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+
|
/// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+
|
||||||
/// * `is_file_transfer` - If the session is used for file transfer.
|
/// * `is_file_transfer` - If the session is used for file transfer.
|
||||||
pub fn start(identifier: &str, is_file_transfer: bool, events2ui: StreamSink<EventToUI>) {
|
/// * `is_port_forward` - If the session is used for port forward.
|
||||||
|
pub fn start(
|
||||||
|
identifier: &str,
|
||||||
|
is_file_transfer: bool,
|
||||||
|
is_port_forward: bool,
|
||||||
|
events2ui: StreamSink<EventToUI>,
|
||||||
|
) {
|
||||||
// TODO check same id
|
// TODO check same id
|
||||||
let session_id = get_session_id(identifier.to_owned());
|
let session_id = get_session_id(identifier.to_owned());
|
||||||
LocalConfig::set_remote_id(&session_id);
|
LocalConfig::set_remote_id(&session_id);
|
||||||
@ -83,17 +89,17 @@ impl Session {
|
|||||||
lc: Default::default(),
|
lc: Default::default(),
|
||||||
events2ui,
|
events2ui,
|
||||||
};
|
};
|
||||||
session
|
session.lc.write().unwrap().initialize(
|
||||||
.lc
|
session_id.clone(),
|
||||||
.write()
|
is_file_transfer,
|
||||||
.unwrap()
|
is_port_forward,
|
||||||
.initialize(session_id.clone(), is_file_transfer, false);
|
);
|
||||||
SESSIONS
|
SESSIONS
|
||||||
.write()
|
.write()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert(identifier.to_owned(), session.clone());
|
.insert(identifier.to_owned(), session.clone());
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
Connection::start(session, is_file_transfer);
|
Connection::start(session, is_file_transfer, is_port_forward);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,7 +207,7 @@ impl Session {
|
|||||||
self.send(Data::Close);
|
self.send(Data::Close);
|
||||||
let session = self.clone();
|
let session = self.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
Connection::start(session, false);
|
Connection::start(session, false, false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -719,18 +725,21 @@ impl Connection {
|
|||||||
///
|
///
|
||||||
/// * `session` - The session to create a new connection for.
|
/// * `session` - The session to create a new connection for.
|
||||||
/// * `is_file_transfer` - Whether the connection is for file transfer.
|
/// * `is_file_transfer` - Whether the connection is for file transfer.
|
||||||
|
/// * `is_port_forward` - Whether the connection is for port forward.
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn start(session: Session, is_file_transfer: bool) {
|
async fn start(session: Session, is_file_transfer: bool, is_port_forward: bool) {
|
||||||
let mut last_recv_time = Instant::now();
|
let mut last_recv_time = Instant::now();
|
||||||
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
|
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
|
||||||
let mut stop_clipboard = None;
|
let mut stop_clipboard = None;
|
||||||
if !is_file_transfer {
|
if !is_file_transfer && !is_port_forward {
|
||||||
stop_clipboard = Self::start_clipboard(sender.clone(), session.lc.clone());
|
stop_clipboard = Self::start_clipboard(sender.clone(), session.lc.clone());
|
||||||
}
|
}
|
||||||
*session.sender.write().unwrap() = Some(sender);
|
*session.sender.write().unwrap() = Some(sender);
|
||||||
let conn_type = if is_file_transfer {
|
let conn_type = if is_file_transfer {
|
||||||
session.lc.write().unwrap().is_file_transfer = true;
|
session.lc.write().unwrap().is_file_transfer = true;
|
||||||
ConnType::FILE_TRANSFER
|
ConnType::FILE_TRANSFER
|
||||||
|
} else if is_port_forward {
|
||||||
|
ConnType::PORT_FORWARD // TODO: RDP
|
||||||
} else {
|
} else {
|
||||||
ConnType::DEFAULT_CONN
|
ConnType::DEFAULT_CONN
|
||||||
};
|
};
|
||||||
|
@ -111,8 +111,9 @@ pub fn session_connect(
|
|||||||
events2ui: StreamSink<EventToUI>,
|
events2ui: StreamSink<EventToUI>,
|
||||||
id: String,
|
id: String,
|
||||||
is_file_transfer: bool,
|
is_file_transfer: bool,
|
||||||
|
is_port_forward: bool,
|
||||||
) -> ResultType<()> {
|
) -> ResultType<()> {
|
||||||
Session::start(&id, is_file_transfer, events2ui);
|
Session::start(&id, is_file_transfer, is_port_forward, events2ui);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -592,12 +593,41 @@ pub fn main_load_lan_peers() {
|
|||||||
{
|
{
|
||||||
let data = HashMap::from([
|
let data = HashMap::from([
|
||||||
("name", "load_lan_peers".to_owned()),
|
("name", "load_lan_peers".to_owned()),
|
||||||
("peers", serde_json::to_string(&get_lan_peers()).unwrap_or_default()),
|
(
|
||||||
|
"peers",
|
||||||
|
serde_json::to_string(&get_lan_peers()).unwrap_or_default(),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned()));
|
s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned()));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn main_add_port_forward(id: String, local_port: i32, remote_host: String, remote_port: i32) {
|
||||||
|
let mut config = get_peer(id.clone());
|
||||||
|
if config
|
||||||
|
.port_forwards
|
||||||
|
.iter()
|
||||||
|
.filter(|x| x.0 == local_port)
|
||||||
|
.next()
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let pf = (local_port, remote_host, remote_port);
|
||||||
|
config.port_forwards.push(pf);
|
||||||
|
config.store(&id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main_remove_port_forward(id: String, local_port: i32) {
|
||||||
|
let mut config = get_peer(id.clone());
|
||||||
|
config.port_forwards = config
|
||||||
|
.port_forwards
|
||||||
|
.drain(..)
|
||||||
|
.filter(|x| x.0 != local_port)
|
||||||
|
.collect();
|
||||||
|
config.store(&id);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn main_get_last_remote_id() -> String {
|
pub fn main_get_last_remote_id() -> String {
|
||||||
// if !config::APP_DIR.read().unwrap().is_empty() {
|
// if !config::APP_DIR.read().unwrap().is_empty() {
|
||||||
// res = LocalConfig::get_remote_id();
|
// res = LocalConfig::get_remote_id();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user