This commit is contained in:
Asura
2022-09-01 23:53:55 -07:00
64 changed files with 8523 additions and 5283 deletions

View File

@@ -33,7 +33,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
final _idController = TextEditingController();
/// Update url. If it's not null, means an update is available.
var _updateUrl = '';
final _updateUrl = '';
Timer? _updateTimer;
@@ -92,7 +92,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
if (snapshot.hasData) {
return snapshot.data!;
} else {
return Offstage();
return const Offstage();
}
}),
],
@@ -110,7 +110,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
/// Callback for the connect button.
/// Connects to the selected peer.
void onConnect({bool isFileTransfer = false}) {
var id = _idController.text.trim();
final id = _idController.text.trim();
connect(id, isFileTransfer: isFileTransfer);
}
@@ -120,9 +120,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
if (id == '') return;
id = id.replaceAll(' ', '');
if (isFileTransfer) {
await rustDeskWinManager.new_file_transfer(id);
await rustDeskWinManager.newFileTransfer(id);
} else {
await rustDeskWinManager.new_remote_desktop(id);
await rustDeskWinManager.newRemoteDesktop(id);
}
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
@@ -233,7 +233,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
},
child: Container(
height: 24,
width: 72,
alignment: Alignment.center,
decoration: BoxDecoration(
color: ftPressed.value
@@ -257,7 +256,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
color: ftPressed.value
? MyTheme.color(context).bg
: MyTheme.color(context).text),
),
).marginSymmetric(horizontal: 12),
),
)),
SizedBox(
@@ -272,7 +271,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
onTap: onConnect,
child: Container(
height: 24,
width: 65,
decoration: BoxDecoration(
color: connPressed.value
? MyTheme.accent
@@ -289,12 +287,12 @@ class _ConnectionPageState extends State<ConnectionPage> {
child: Center(
child: Text(
translate(
"Connection",
"Connect",
),
style: TextStyle(
fontSize: 12, color: MyTheme.color(context).bg),
),
),
).marginSymmetric(horizontal: 12),
),
),
),

View File

@@ -3,14 +3,13 @@ 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/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart';
import '../../models/model.dart';
class ConnectionTabPage extends StatefulWidget {
final Map<String, dynamic> params;
@@ -22,26 +21,27 @@ class ConnectionTabPage extends StatefulWidget {
class _ConnectionTabPageState extends State<ConnectionTabPage> {
final tabController = Get.put(DesktopTabController());
static final Rx<String> _fullscreenID = "".obs;
static final IconData selectedIcon = Icons.desktop_windows_sharp;
static final IconData unselectedIcon = Icons.desktop_windows_outlined;
static const IconData selectedIcon = Icons.desktop_windows_sharp;
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
var connectionMap = RxList<Widget>.empty(growable: true);
_ConnectionTabPageState(Map<String, dynamic> params) {
if (params['id'] != null) {
final RxBool fullscreen = Get.find(tag: 'fullscreen');
final peerId = params['id'];
if (peerId != null) {
ConnectionTypeState.init(peerId);
tabController.add(TabInfo(
key: params['id'],
label: params['id'],
key: peerId,
label: peerId,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
page: RemotePage(
key: ValueKey(params['id']),
id: params['id'],
tabBarHeight:
_fullscreenID.value.isNotEmpty ? 0 : kDesktopRemoteTabBarHeight,
fullscreenID: _fullscreenID,
)));
page: Obx(() => RemotePage(
key: ValueKey(peerId),
id: peerId,
tabBarHeight:
fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight,
))));
}
}
@@ -54,33 +54,27 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
print(
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
final RxBool fullscreen = Get.find(tag: 'fullscreen');
// for simplify, just replace connectionId
if (call.method == "new_remote_desktop") {
final args = jsonDecode(call.arguments);
final id = args['id'];
window_on_top(windowId());
ConnectionTypeState.init(id);
tabController.add(TabInfo(
key: id,
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
page: RemotePage(
key: ValueKey(id),
id: id,
tabBarHeight: _fullscreenID.value.isNotEmpty
? 0
: kDesktopRemoteTabBarHeight,
fullscreenID: _fullscreenID,
)));
page: Obx(() => RemotePage(
key: ValueKey(id),
id: id,
tabBarHeight:
fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight,
))));
} 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();
tabController.clear();
}
});
}
@@ -88,36 +82,79 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
@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: Obx(() => DesktopTab(
controller: tabController,
theme: theme,
isMainWindow: false,
showTabBar: _fullscreenID.value.isEmpty,
tail: AddButton(
theme: theme,
).paddingOnly(left: 10),
pageViewBuilder: (pageView) {
WindowController.fromWindowId(windowId())
.setFullscreen(_fullscreenID.value.isNotEmpty);
return pageView;
},
))),
),
);
final RxBool fullscreen = Get.find(tag: 'fullscreen');
return Obx(() => SubWindowDragToResizeArea(
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
windowId: windowId(),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: MyTheme.color(context).bg,
body: Obx(() => DesktopTab(
controller: tabController,
theme: theme,
tabType: DesktopTabType.remoteScreen,
showTabBar: fullscreen.isFalse,
onClose: () {
tabController.clear();
},
tail: AddButton(
theme: theme,
).paddingOnly(left: 10),
pageViewBuilder: (pageView) {
WindowController.fromWindowId(windowId())
.setFullscreen(fullscreen.isTrue);
return pageView;
},
tabBuilder: (key, icon, label, themeConf) => Obx(() {
final connectionType = ConnectionTypeState.find(key);
if (!connectionType.isValid()) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
label,
],
);
} else {
final msgDirect = translate(
connectionType.direct.value ==
ConnectionType.strDirect
? 'Direct Connection'
: 'Relay Connection');
final msgSecure = translate(
connectionType.secure.value ==
ConnectionType.strSecure
? 'Secure Connection'
: 'Insecure Connection');
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
Tooltip(
message: '$msgDirect\n$msgSecure',
child: Image.asset(
'assets/${connectionType.secure.value}${connectionType.direct.value}.png',
width: themeConf.iconSize,
height: themeConf.iconSize,
).paddingOnly(right: 5),
),
label,
],
);
}
}),
))),
),
));
}
void onRemoveId(String id) {
ffi(id).close();
if (tabController.state.value.tabs.length == 0) {
WindowController.fromWindowId(windowId()).close();
if (tabController.state.value.tabs.isEmpty) {
WindowController.fromWindowId(windowId()).hide();
}
ConnectionTypeState.delete(id);
}
int windowId() {

View File

@@ -806,6 +806,8 @@ Future<bool> loginDialog() async {
var userNameMsg = "";
String pass = "";
var passMsg = "";
var userContontroller = TextEditingController(text: userName);
var pwdController = TextEditingController(text: pass);
var isInProgress = false;
var completer = Completer<bool>();
@@ -833,13 +835,10 @@ Future<bool> loginDialog() async {
),
Expanded(
child: TextField(
onChanged: (s) {
userName = s;
},
decoration: InputDecoration(
border: OutlineInputBorder(),
errorText: userNameMsg.isNotEmpty ? userNameMsg : null),
controller: TextEditingController(text: userName),
controller: userContontroller,
),
),
],
@@ -859,13 +858,10 @@ Future<bool> loginDialog() async {
Expanded(
child: TextField(
obscureText: true,
onChanged: (s) {
pass = s;
},
decoration: InputDecoration(
border: OutlineInputBorder(),
errorText: passMsg.isNotEmpty ? passMsg : null),
controller: TextEditingController(text: pass),
controller: pwdController,
),
),
],
@@ -896,8 +892,8 @@ Future<bool> loginDialog() async {
isInProgress = false;
});
};
userName = userName;
pass = pass;
userName = userContontroller.text;
pass = pwdController.text;
if (userName.isEmpty) {
userNameMsg = translate("Username missed");
cancel();

View File

@@ -1025,7 +1025,6 @@ class _ComboBox extends StatelessWidget {
void changeServer() async {
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
print("${oldOptions}");
String idServer = oldOptions['custom-rendezvous-server'] ?? "";
var idServerMsg = "";
String relayServer = oldOptions['relay-server'] ?? "";
@@ -1033,6 +1032,10 @@ void changeServer() async {
String apiServer = oldOptions['api-server'] ?? "";
var apiServerMsg = "";
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;
gFFI.dialogManager.show((setState, close) {
@@ -1057,13 +1060,10 @@ void changeServer() async {
),
Expanded(
child: TextField(
onChanged: (s) {
idServer = s;
},
decoration: InputDecoration(
border: OutlineInputBorder(),
errorText: idServerMsg.isNotEmpty ? idServerMsg : null),
controller: TextEditingController(text: idServer),
controller: idController,
),
),
],
@@ -1082,14 +1082,11 @@ void changeServer() async {
),
Expanded(
child: TextField(
onChanged: (s) {
relayServer = s;
},
decoration: InputDecoration(
border: OutlineInputBorder(),
errorText:
relayServerMsg.isNotEmpty ? relayServerMsg : null),
controller: TextEditingController(text: relayServer),
controller: relayController,
),
),
],
@@ -1108,14 +1105,11 @@ void changeServer() async {
),
Expanded(
child: TextField(
onChanged: (s) {
apiServer = s;
},
decoration: InputDecoration(
border: OutlineInputBorder(),
errorText:
apiServerMsg.isNotEmpty ? apiServerMsg : null),
controller: TextEditingController(text: apiServer),
controller: apiController,
),
),
],
@@ -1134,13 +1128,10 @@ void changeServer() async {
),
Expanded(
child: TextField(
onChanged: (s) {
key = s;
},
decoration: InputDecoration(
border: OutlineInputBorder(),
),
controller: TextEditingController(text: key),
controller: keyController,
),
),
],
@@ -1171,10 +1162,10 @@ void changeServer() async {
isInProgress = false;
});
};
idServer = idServer.trim();
relayServer = relayServer.trim();
apiServer = apiServer.trim();
key = key.trim();
idServer = idController.text.trim();
relayServer = relayController.text.trim();
apiServer = apiController.text.trim().toLowerCase();
key = keyController.text.trim();
if (idServer.isNotEmpty) {
idServerMsg = translate(
@@ -1230,6 +1221,7 @@ void changeWhiteList() async {
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(',');
var newWhiteListField = newWhiteList.join('\n');
var controller = TextEditingController(text: newWhiteListField);
var msg = "";
var isInProgress = false;
gFFI.dialogManager.show((setState, close) {
@@ -1246,15 +1238,12 @@ void changeWhiteList() async {
children: [
Expanded(
child: TextField(
onChanged: (s) {
newWhiteListField = s;
},
maxLines: null,
decoration: InputDecoration(
border: OutlineInputBorder(),
errorText: msg.isEmpty ? null : translate(msg),
),
controller: TextEditingController(text: newWhiteListField),
controller: controller,
),
),
],
@@ -1277,7 +1266,7 @@ void changeWhiteList() async {
msg = "";
isInProgress = true;
});
newWhiteListField = newWhiteListField.trim();
newWhiteListField = controller.text.trim();
var newWhiteList = "";
if (newWhiteListField.isEmpty) {
// pass
@@ -1319,6 +1308,9 @@ void changeSocks5Proxy() async {
username = socks[1];
password = socks[2];
}
var proxyController = TextEditingController(text: proxy);
var userController = TextEditingController(text: username);
var pwdController = TextEditingController(text: password);
var isInProgress = false;
gFFI.dialogManager.show((setState, close) {
@@ -1343,13 +1335,10 @@ void changeSocks5Proxy() async {
),
Expanded(
child: TextField(
onChanged: (s) {
proxy = s;
},
decoration: InputDecoration(
border: OutlineInputBorder(),
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
controller: TextEditingController(text: proxy),
controller: proxyController,
),
),
],
@@ -1368,13 +1357,10 @@ void changeSocks5Proxy() async {
),
Expanded(
child: TextField(
onChanged: (s) {
username = s;
},
decoration: InputDecoration(
border: OutlineInputBorder(),
),
controller: TextEditingController(text: username),
controller: userController,
),
),
],
@@ -1393,13 +1379,10 @@ void changeSocks5Proxy() async {
),
Expanded(
child: TextField(
onChanged: (s) {
password = s;
},
decoration: InputDecoration(
border: OutlineInputBorder(),
),
controller: TextEditingController(text: password),
controller: pwdController,
),
),
],
@@ -1428,9 +1411,9 @@ void changeSocks5Proxy() async {
isInProgress = false;
});
};
proxy = proxy.trim();
username = username.trim();
password = password.trim();
proxy = proxyController.text.trim();
username = userController.text.trim();
password = pwdController.text.trim();
if (proxy.isNotEmpty) {
proxyMsg =

View File

@@ -4,6 +4,7 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
class DesktopTabPage extends StatefulWidget {
@@ -33,26 +34,29 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
@override
Widget build(BuildContext context) {
final dark = isDarkTheme();
return DragToResizeArea(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: MyTheme.color(context).bg,
body: DesktopTab(
controller: tabController,
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
isMainWindow: true,
tail: ActionIcon(
message: 'Settings',
icon: IconFont.menu,
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
onTap: onAddSetting,
is_close: false,
),
)),
),
);
RxBool fullscreen = false.obs;
Get.put(fullscreen, tag: 'fullscreen');
return Obx(() => DragToResizeArea(
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: MyTheme.color(context).bg,
body: DesktopTab(
controller: tabController,
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
tabType: DesktopTabType.main,
tail: ActionIcon(
message: 'Settings',
icon: IconFont.menu,
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
onTap: onAddSetting,
is_close: false,
),
)),
),
));
}
void onAddSetting() {

View File

@@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/file_manager_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';
@@ -20,12 +19,13 @@ class FileManagerTabPage extends StatefulWidget {
}
class _FileManagerTabPageState extends State<FileManagerTabPage> {
final tabController = Get.put(DesktopTabController());
DesktopTabController get tabController => Get.find<DesktopTabController>();
static final IconData selectedIcon = Icons.file_copy_sharp;
static final IconData unselectedIcon = Icons.file_copy_outlined;
_FileManagerTabPageState(Map<String, dynamic> params) {
Get.put(DesktopTabController());
tabController.add(TabInfo(
key: params['id'],
label: params['id'],
@@ -42,7 +42,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
print(
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
"call ${call.method} with args ${call.arguments} from window ${fromWindowId} to ${windowId()}");
// for simplify, just replace connectionId
if (call.method == "new_file_transfer") {
final args = jsonDecode(call.arguments);
@@ -55,21 +55,15 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
unselectedIcon: unselectedIcon,
page: FileManagerPage(key: ValueKey(id), id: id)));
} 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();
tabController.clear();
}
});
}
@override
Widget build(BuildContext context) {
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
final theme =
isDarkTheme() ? const TarBarTheme.dark() : const TarBarTheme.light();
return SubWindowDragToResizeArea(
windowId: windowId(),
child: Container(
@@ -80,7 +74,10 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
body: DesktopTab(
controller: tabController,
theme: theme,
isMainWindow: false,
tabType: DesktopTabType.fileTransfer,
onClose: () {
tabController.clear();
},
tail: AddButton(
theme: theme,
).paddingOnly(left: 10),
@@ -90,9 +87,8 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
}
void onRemoveId(String id) {
ffi("ft_$id").close();
if (tabController.state.value.tabs.length == 0) {
WindowController.fromWindowId(windowId()).close();
if (tabController.state.value.tabs.isEmpty) {
WindowController.fromWindowId(windowId()).hide();
}
}

View 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.sessionAddPortForward(
id: 'pf_${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.sessionRemovePortForward(
id: 'pf_${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;
}

View File

@@ -0,0 +1,103 @@
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/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());
late final bool isRDP;
static const IconData selectedIcon = Icons.forward_sharp;
static const IconData unselectedIcon = Icons.forward_outlined;
_PortForwardTabPageState(Map<String, dynamic> params) {
isRDP = params['isRDP'];
tabController.add(TabInfo(
key: params['id'],
label: params['id'],
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
page: PortForwardPage(
key: ValueKey(params['id']),
id: params['id'],
isRDP: isRDP,
)));
}
@override
void initState() {
super.initState();
tabController.onRemove = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
debugPrint(
"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.clear();
}
});
}
@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,
tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward,
onClose: () {
tabController.clear();
},
tail: AddButton(
theme: theme,
).paddingOnly(left: 10),
)),
),
);
}
void onRemoveId(String id) {
ffi("pf_$id").close();
if (tabController.state.value.tabs.isEmpty) {
WindowController.fromWindowId(windowId()).hide();
}
}
int windowId() {
return widget.params["windowId"];
}
}

View File

@@ -5,32 +5,32 @@ import 'dart:ui' as ui;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock/wakelock.dart';
// import 'package:window_manager/window_manager.dart';
import '../widgets/remote_menubar.dart';
import '../../common.dart';
import '../../mobile/widgets/dialog.dart';
import '../../mobile/widgets/overlay.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../models/chat_model.dart';
import '../../common/shared_state.dart';
final initText = '\1' * 1024;
class RemotePage extends StatefulWidget {
RemotePage(
{Key? key,
required this.id,
required this.tabBarHeight,
required this.fullscreenID})
: super(key: key);
RemotePage({
Key? key,
required this.id,
required this.tabBarHeight,
}) : super(key: key);
final String id;
final double tabBarHeight;
final Rx<String> fullscreenID;
@override
_RemotePageState createState() => _RemotePageState();
@@ -41,7 +41,7 @@ class _RemotePageState extends State<RemotePage>
Timer? _timer;
bool _showBar = !isWebDesktop;
String _value = '';
var _cursorOverImage = false.obs;
final _cursorOverImage = false.obs;
final FocusNode _mobileFocusNode = FocusNode();
final FocusNode _physicalFocusNode = FocusNode();
@@ -50,11 +50,27 @@ class _RemotePageState extends State<RemotePage>
late FFI _ffi;
void _updateTabBarHeight() {
_ffi.canvasModel.tabBarHeight = widget.tabBarHeight;
}
void _initStates(String id) {
PrivacyModeState.init(id);
BlockInputState.init(id);
CurrentDisplayState.init(id);
}
void _removeStates(String id) {
PrivacyModeState.delete(id);
BlockInputState.delete(id);
CurrentDisplayState.delete(id);
}
@override
void initState() {
super.initState();
_ffi = FFI();
_ffi.canvasModel.tabBarHeight = super.widget.tabBarHeight;
_updateTabBarHeight();
Get.put(_ffi, tag: widget.id);
_ffi.connect(widget.id, tabBarHeight: super.widget.tabBarHeight);
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -70,11 +86,12 @@ class _RemotePageState extends State<RemotePage>
_ffi.listenToMouse(true);
_ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
// WindowManager.instance.addListener(this);
_initStates(widget.id);
}
@override
void dispose() {
print("REMOTE PAGE dispose ${widget.id}");
debugPrint("REMOTE PAGE dispose ${widget.id}");
hideMobileActionsOverlay();
_ffi.listenToMouse(false);
_mobileFocusNode.dispose();
@@ -90,6 +107,7 @@ class _RemotePageState extends State<RemotePage>
// WindowManager.instance.removeListener(this);
Get.delete<FFI>(tag: widget.id);
super.dispose();
_removeStates(widget.id);
}
void resetTool() {
@@ -187,19 +205,19 @@ class _RemotePageState extends State<RemotePage>
return Scaffold(
backgroundColor: MyTheme.color(context).bg,
// resizeToAvoidBottomInset: true,
floatingActionButton: _showBar
? null
: FloatingActionButton(
mini: true,
child: Icon(Icons.expand_less),
backgroundColor: MyTheme.accent,
onPressed: () {
setState(() {
_showBar = !_showBar;
});
}),
bottomNavigationBar:
_showBar && hasDisplays ? getBottomAppBar(ffiModel) : null,
// floatingActionButton: _showBar
// ? null
// : FloatingActionButton(
// mini: true,
// child: Icon(Icons.expand_less),
// backgroundColor: MyTheme.accent,
// onPressed: () {
// setState(() {
// _showBar = !_showBar;
// });
// }),
// bottomNavigationBar:
// _showBar && hasDisplays ? getBottomAppBar(ffiModel) : null,
body: Overlay(
initialEntries: [
OverlayEntry(builder: (context) {
@@ -217,6 +235,7 @@ class _RemotePageState extends State<RemotePage>
@override
Widget build(BuildContext context) {
super.build(context);
_updateTabBarHeight();
return WillPopScope(
onWillPop: () async {
clientClose(_ffi.dialogManager);
@@ -337,6 +356,7 @@ class _RemotePageState extends State<RemotePage>
}
Widget? getBottomAppBar(FfiModel ffiModel) {
final RxBool fullscreen = Get.find(tag: 'fullscreen');
return MouseRegion(
cursor: SystemMouseCursors.basic,
child: BottomAppBar(
@@ -371,15 +391,11 @@ class _RemotePageState extends State<RemotePage>
: <Widget>[
IconButton(
color: Colors.white,
icon: Icon(widget.fullscreenID.value.isEmpty
icon: Icon(fullscreen.isTrue
? Icons.fullscreen
: Icons.close_fullscreen),
onPressed: () {
if (widget.fullscreenID.value.isEmpty) {
widget.fullscreenID.value = widget.id;
} else {
widget.fullscreenID.value = "";
}
fullscreen.value = !fullscreen.value;
},
)
]) +
@@ -452,7 +468,7 @@ class _RemotePageState extends State<RemotePage>
}
if (_isPhysicalMouse) {
_ffi.handleMouse(getEvent(e, 'mousemove'),
tabBarHeight: super.widget.tabBarHeight);
tabBarHeight: widget.tabBarHeight);
}
}
@@ -466,7 +482,7 @@ class _RemotePageState extends State<RemotePage>
}
if (_isPhysicalMouse) {
_ffi.handleMouse(getEvent(e, 'mousedown'),
tabBarHeight: super.widget.tabBarHeight);
tabBarHeight: widget.tabBarHeight);
}
}
@@ -474,7 +490,7 @@ class _RemotePageState extends State<RemotePage>
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (_isPhysicalMouse) {
_ffi.handleMouse(getEvent(e, 'mouseup'),
tabBarHeight: super.widget.tabBarHeight);
tabBarHeight: widget.tabBarHeight);
}
}
@@ -482,7 +498,7 @@ class _RemotePageState extends State<RemotePage>
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (_isPhysicalMouse) {
_ffi.handleMouse(getEvent(e, 'mousemove'),
tabBarHeight: super.widget.tabBarHeight);
tabBarHeight: widget.tabBarHeight);
}
}
@@ -548,6 +564,10 @@ class _RemotePageState extends State<RemotePage>
));
}
paints.add(QualityMonitor(_ffi.qualityMonitorModel));
paints.add(RemoteMenubar(
id: widget.id,
ffi: _ffi,
));
return Stack(
children: paints,
);
@@ -717,11 +737,11 @@ class ImagePaint extends StatelessWidget {
width: c.getDisplayWidth() * s,
height: c.getDisplayHeight() * s,
child: CustomPaint(
painter: new ImagePainter(image: m.image, x: 0, y: 0, scale: s),
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
));
return Center(
child: NotificationListener<ScrollNotification>(
onNotification: (_notification) {
onNotification: (notification) {
final percentX = _horizontal.position.extentBefore /
(_horizontal.position.extentBefore +
_horizontal.position.extentInside +
@@ -744,8 +764,8 @@ class ImagePaint extends StatelessWidget {
width: c.size.width,
height: c.size.height,
child: CustomPaint(
painter: new ImagePainter(
image: m.image, x: c.x / s, y: c.y / s, scale: s),
painter:
ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
));
return _buildListener(imageWidget);
}
@@ -799,7 +819,7 @@ class CursorPaint extends StatelessWidget {
// final adjust = m.adjustForKeyboard();
var s = c.scale;
return CustomPaint(
painter: new ImagePainter(
painter: ImagePainter(
image: m.image,
x: m.x * s - m.hotx + c.x,
y: m.y * s - m.hoty + c.y,
@@ -824,15 +844,16 @@ class ImagePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
if (image == null) return;
if (x.isNaN || y.isNaN) return;
canvas.scale(scale, scale);
// https://github.com/flutter/flutter/issues/76187#issuecomment-784628161
// https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html
var paint = new Paint();
var paint = Paint();
paint.filterQuality = FilterQuality.medium;
if (scale > 10.00000) {
paint.filterQuality = FilterQuality.high;
}
canvas.drawImage(image!, new Offset(x, y), paint);
canvas.drawImage(image!, Offset(x, y), paint);
}
@override

View File

@@ -111,7 +111,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
showMaximize: false,
showMinimize: false,
controller: serverModel.tabController,
isMainWindow: true,
tabType: DesktopTabType.cm,
pageViewBuilder: (pageView) => Row(children: [
Expanded(child: pageView),
Consumer<ChatModel>(
@@ -294,7 +294,8 @@ class _CmHeaderState extends State<_CmHeader>
Offstage(
offstage: client.isFileTransfer,
child: IconButton(
onPressed: () => gFFI.chatModel.toggleCMChatPage(client.id),
onPressed: () => checkClickTime(
client.id, () => gFFI.chatModel.toggleCMChatPage(client.id)),
icon: Icon(Icons.message_outlined),
),
)
@@ -326,7 +327,8 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey),
padding: EdgeInsets.all(4.0),
child: InkWell(
onTap: () => onTap?.call(!enabled),
onTap: () =>
checkClickTime(widget.client.id, () => onTap?.call(!enabled)),
child: Image(
image: icon,
width: 50,
@@ -422,7 +424,8 @@ class _CmControlPanel extends StatelessWidget {
decoration: BoxDecoration(
color: Colors.redAccent, borderRadius: BorderRadius.circular(10)),
child: InkWell(
onTap: () => handleDisconnect(context),
onTap: () =>
checkClickTime(client.id, () => handleDisconnect(context)),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -447,7 +450,8 @@ class _CmControlPanel extends StatelessWidget {
decoration: BoxDecoration(
color: MyTheme.accent, borderRadius: BorderRadius.circular(10)),
child: InkWell(
onTap: () => handleAccept(context),
onTap: () =>
checkClickTime(client.id, () => handleAccept(context)),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -469,7 +473,8 @@ class _CmControlPanel extends StatelessWidget {
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey)),
child: InkWell(
onTap: () => handleDisconnect(context),
onTap: () =>
checkClickTime(client.id, () => handleDisconnect(context)),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -572,3 +577,12 @@ Widget clientInfo(Client client) {
),
]));
}
void checkClickTime(int id, Function() callback) async {
var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
await bind.cmCheckClickTime(connId: id);
Timer(const Duration(milliseconds: 120), () async {
var d = clickCallbackTime - await bind.cmGetClickTime();
if (d > 120) callback();
});
}