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

View File

@ -32,10 +32,10 @@ RustDesk welcomes contribution from everyone. See [`CONTRIBUTING.md`](CONTRIBUTI
Below are the servers you are using for free, it may change along the time. If you are not close to one of these, your network may be slow.
| Location | Vendor | Specification |
| --------- | ------------- | ------------------ |
| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM |
| Singapore | Vultr | 1 VCPU / 1GB RAM |
| Germany | Hetzner | 2 VCPU / 4GB RAM |
| Germany | Codext | 4 VCPU / 8GB RAM |
| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
| Singapore | Vultr | 1 vCPU / 1GB RAM |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
| Germany | Codext | 4 vCPU / 8GB RAM |
## Dependencies

2
flutter/.gitignore vendored
View File

@ -45,7 +45,7 @@ jniLibs
# flutter rust bridge
# Flutter Generated Files
**/flutter/GeneratedPluginRegistrant.swift
**/GeneratedPluginRegistrant.swift
**/flutter/generated_plugin_registrant.cc
**/flutter/generated_plugin_registrant.h
**/flutter/generated_plugins.cmake

View File

@ -427,7 +427,45 @@ class CustomAlertDialog extends StatelessWidget {
void msgBox(
String type, String title, String text, OverlayDialogManager dialogManager,
{bool? hasCancel}) {
var wrap = (String text, void Function() onPressed) => ButtonTheme(
dialogManager.dismissAll();
List<Widget> buttons = [];
if (type != "connecting" && type != "success" && !type.contains("nook")) {
buttons.insert(
0,
msgBoxButton(translate('OK'), () {
dialogManager.dismissAll();
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
if (!type.contains("custom")) {
closeConnection();
}
}));
}
hasCancel ??= !type.contains("error") &&
!type.contains("nocancel") &&
type != "restarting";
if (hasCancel) {
buttons.insert(
0,
msgBoxButton(translate('Cancel'), () {
dialogManager.dismissAll();
}));
}
// TODO: test this button
if (type.contains("hasclose")) {
buttons.insert(
0,
msgBoxButton(translate('Close'), () {
dialogManager.dismissAll();
}));
}
dialogManager.show((setState, close) => CustomAlertDialog(
title: _msgBoxTitle(title),
content: Text(translate(text), style: TextStyle(fontSize: 15)),
actions: buttons));
}
Widget msgBoxButton(String text, void Function() onPressed) {
return ButtonTheme(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
//limits the touch area to the button area
@ -439,41 +477,16 @@ void msgBox(
onPressed: onPressed,
child:
Text(translate(text), style: TextStyle(color: MyTheme.accent))));
}
Widget _msgBoxTitle(String title) => Text(translate(title), style: TextStyle(fontSize: 21));
void msgBoxCommon(OverlayDialogManager dialogManager, String title,
Widget content, List<Widget> buttons) {
dialogManager.dismissAll();
List<Widget> buttons = [];
if (type != "connecting" && type != "success" && type.indexOf("nook") < 0) {
buttons.insert(
0,
wrap(translate('OK'), () {
dialogManager.dismissAll();
closeConnection();
}));
}
if (hasCancel == null) {
// hasCancel = type != 'error';
hasCancel = type.indexOf("error") < 0 &&
type.indexOf("nocancel") < 0 &&
type != "restarting";
}
if (hasCancel) {
buttons.insert(
0,
wrap(translate('Cancel'), () {
dialogManager.dismissAll();
}));
}
// TODO: test this button
if (type.indexOf("hasclose") >= 0) {
buttons.insert(
0,
wrap(translate('Close'), () {
dialogManager.dismissAll();
}));
}
dialogManager.show((setState, close) => CustomAlertDialog(
title: Text(translate(title), style: TextStyle(fontSize: 21)),
content: Text(translate(text), style: TextStyle(fontSize: 15)),
title: _msgBoxTitle(title),
content: content,
actions: buttons));
}
@ -492,13 +505,13 @@ const G = M * K;
String readableFileSize(double size) {
if (size < K) {
return size.toStringAsFixed(2) + " B";
return "${size.toStringAsFixed(2)} B";
} else if (size < M) {
return (size / K).toStringAsFixed(2) + " KB";
return "${(size / K).toStringAsFixed(2)} KB";
} else if (size < G) {
return (size / M).toStringAsFixed(2) + " MB";
return "${(size / M).toStringAsFixed(2)} MB";
} else {
return (size / G).toStringAsFixed(2) + " GB";
return "${(size / G).toStringAsFixed(2)} GB";
}
}
@ -661,8 +674,6 @@ Future<void> initGlobalFFI() async {
debugPrint("_globalFFI init end");
// after `put`, can also be globally found by Get.find<FFI>();
Get.put(_globalFFI, permanent: true);
// trigger connection status updater
await bind.mainCheckConnectStatus();
// global shared preference
await Get.putAsync(() => SharedPreferences.getInstance());
}

View File

@ -0,0 +1,87 @@
import 'package:get/get.dart';
import '../consts.dart';
class PrivacyModeState {
static String tag(String id) => 'privacy_mode_$id';
static void init(String id) {
final RxBool state = false.obs;
Get.put(state, tag: tag(id));
}
static void delete(String id) => Get.delete(tag: tag(id));
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class BlockInputState {
static String tag(String id) => 'block_input_$id';
static void init(String id) {
final RxBool state = false.obs;
Get.put(state, tag: tag(id));
}
static void delete(String id) => Get.delete(tag: tag(id));
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class CurrentDisplayState {
static String tag(String id) => 'current_display_$id';
static void init(String id) {
final RxInt state = RxInt(0);
Get.put(state, tag: tag(id));
}
static void delete(String id) => Get.delete(tag: tag(id));
static RxInt find(String id) => Get.find<RxInt>(tag: tag(id));
}
class ConnectionType {
final Rx<String> _secure = kInvalidValueStr.obs;
final Rx<String> _direct = kInvalidValueStr.obs;
Rx<String> get secure => _secure;
Rx<String> get direct => _direct;
static String get strSecure => 'secure';
static String get strInsecure => 'insecure';
static String get strDirect => '';
static String get strIndirect => '_relay';
void setSecure(bool v) {
_secure.value = v ? strSecure : strInsecure;
}
void setDirect(bool v) {
_direct.value = v ? strDirect : strIndirect;
}
bool isValid() {
return _secure.value != kInvalidValueStr &&
_direct.value != kInvalidValueStr;
}
}
class ConnectionTypeState {
static String tag(String id) => 'connection_type_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
final ConnectionType collectionType = ConnectionType();
Get.put(collectionType, tag: key);
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
}
}
static ConnectionType find(String id) =>
Get.find<ConnectionType>(tag: tag(id));
}

View File

@ -4,8 +4,14 @@ const double kDesktopRemoteTabBarHeight = 28.0;
const String kAppTypeMain = "main";
const String kAppTypeDesktopRemote = "remote";
const String kAppTypeDesktopFileTransfer = "file transfer";
const String kAppTypeDesktopPortForward = "port forward";
const String kTabLabelHomePage = "Home";
const String kTabLabelSettingPage = "Settings";
const int kDefaultDisplayWidth = 1280;
const int kDefaultDisplayHeight = 720;
const int kMobileDefaultDisplayWidth = 720;
const int kMobileDefaultDisplayHeight = 1280;
const int kDesktopDefaultDisplayWidth = 1080;
const int kDesktopDefaultDisplayHeight = 720;
const kInvalidValueStr = "InvalidValueStr";

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'],
page: Obx(() => RemotePage(
key: ValueKey(peerId),
id: peerId,
tabBarHeight:
_fullscreenID.value.isNotEmpty ? 0 : kDesktopRemoteTabBarHeight,
fullscreenID: _fullscreenID,
)));
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(
page: Obx(() => RemotePage(
key: ValueKey(id),
id: id,
tabBarHeight: _fullscreenID.value.isNotEmpty
? 0
: kDesktopRemoteTabBarHeight,
fullscreenID: _fullscreenID,
)));
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,7 +82,9 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
@override
Widget build(BuildContext context) {
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
return SubWindowDragToResizeArea(
final RxBool fullscreen = Get.find(tag: 'fullscreen');
return Obx(() => SubWindowDragToResizeArea(
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
windowId: windowId(),
child: Container(
decoration: BoxDecoration(
@ -98,26 +94,67 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
body: Obx(() => DesktopTab(
controller: tabController,
theme: theme,
isMainWindow: false,
showTabBar: _fullscreenID.value.isEmpty,
tabType: DesktopTabType.remoteScreen,
showTabBar: fullscreen.isFalse,
onClose: () {
tabController.clear();
},
tail: AddButton(
theme: theme,
).paddingOnly(left: 10),
pageViewBuilder: (pageView) {
WindowController.fromWindowId(windowId())
.setFullscreen(_fullscreenID.value.isNotEmpty);
.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,7 +34,10 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
@override
Widget build(BuildContext context) {
final dark = isDarkTheme();
return DragToResizeArea(
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!)),
@ -42,7 +46,7 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
body: DesktopTab(
controller: tabController,
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
isMainWindow: true,
tabType: DesktopTabType.main,
tail: ActionIcon(
message: 'Settings',
icon: IconFont.menu,
@ -52,7 +56,7 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
),
)),
),
);
));
}
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,
RemotePage({
Key? key,
required this.id,
required this.tabBarHeight,
required this.fullscreenID})
: super(key: key);
}) : 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();
});
}

View 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,
),
),
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/connection_tab_page.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
/// multi-tab desktop remote screen
@ -11,6 +12,8 @@ class DesktopRemoteScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
RxBool fullscreen = false.obs;
Get.put(fullscreen, tag: 'fullscreen');
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: gFFI.ffiModel),

File diff suppressed because it is too large Load Diff

View File

@ -21,18 +21,16 @@ final peerSearchTextController =
TextEditingController(text: peerSearchText.value);
class _PeerWidget extends StatefulWidget {
late final _peers;
late final OffstageFunc _offstageFunc;
late final PeerCardWidgetFunc _peerCardWidgetFunc;
final Peers peers;
final OffstageFunc offstageFunc;
final PeerCardWidgetFunc peerCardWidgetFunc;
_PeerWidget(Peers peers, OffstageFunc offstageFunc,
PeerCardWidgetFunc peerCardWidgetFunc,
{Key? key})
: super(key: key) {
_peers = peers;
_offstageFunc = offstageFunc;
_peerCardWidgetFunc = peerCardWidgetFunc;
}
const _PeerWidget(
{required this.peers,
required this.offstageFunc,
required this.peerCardWidgetFunc,
Key? key})
: super(key: key);
@override
_PeerWidgetState createState() => _PeerWidgetState();
@ -42,9 +40,9 @@ class _PeerWidget extends StatefulWidget {
class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
static const int _maxQueryCount = 3;
var _curPeers = Set<String>();
final _curPeers = <String>{};
var _lastChangeTime = DateTime.now();
var _lastQueryPeers = Set<String>();
var _lastQueryPeers = <String>{};
var _lastQueryTime = DateTime.now().subtract(Duration(hours: 1));
var _queryCoun = 0;
var _exit = false;
@ -78,9 +76,9 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
@override
Widget build(BuildContext context) {
final space = 12.0;
const space = 12.0;
return ChangeNotifierProvider<Peers>(
create: (context) => super.widget._peers,
create: (context) => widget.peers,
child: Consumer<Peers>(
builder: (context, peers, child) => peers.peers.isEmpty
? Center(
@ -96,7 +94,7 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
for (final peer in peers) {
cards.add(Offstage(
key: ValueKey("off${peer.id}"),
offstage: super.widget._offstageFunc(peer),
offstage: widget.offstageFunc(peer),
child: Obx(
() => SizedBox(
width: 220,
@ -116,17 +114,13 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
}
_lastChangeTime = DateTime.now();
},
child: super
.widget
._peerCardWidgetFunc(peer),
child: widget.peerCardWidgetFunc(peer),
),
),
)));
}
return Wrap(
spacing: space,
runSpacing: space,
children: cards);
spacing: space, runSpacing: space, children: cards);
} else {
return const Center(
child: CircularProgressIndicator(),
@ -136,7 +130,8 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
future: matchPeers(searchText.value, peers.peers),
);
}, peerSearchText),
)),
),
),
);
}
@ -175,31 +170,42 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
}
abstract class BasePeerWidget extends StatelessWidget {
late final _name;
late final _loadEvent;
late final OffstageFunc _offstageFunc;
late final PeerCardWidgetFunc _peerCardWidgetFunc;
late final List<Peer> _initPeers;
final String name;
final String loadEvent;
final OffstageFunc offstageFunc;
final PeerCardWidgetFunc peerCardWidgetFunc;
final List<Peer> initPeers;
BasePeerWidget({Key? key}) : super(key: key) {}
const BasePeerWidget({
Key? key,
required this.name,
required this.loadEvent,
required this.offstageFunc,
required this.peerCardWidgetFunc,
required this.initPeers,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return _PeerWidget(Peers(_name, _loadEvent, _initPeers), _offstageFunc,
_peerCardWidgetFunc);
return _PeerWidget(
peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers),
offstageFunc: offstageFunc,
peerCardWidgetFunc: peerCardWidgetFunc);
}
}
class RecentPeerWidget extends BasePeerWidget {
RecentPeerWidget({Key? key}) : super(key: key) {
super._name = "recent peer";
super._loadEvent = "load_recent_peers";
super._offstageFunc = (Peer _peer) => false;
super._peerCardWidgetFunc = (Peer peer) => RecentPeerCard(
RecentPeerWidget({Key? key})
: super(
key: key,
name: 'recent peer',
loadEvent: 'load_recent_peers',
offstageFunc: (Peer peer) => false,
peerCardWidgetFunc: (Peer peer) => RecentPeerCard(
peer: peer,
),
initPeers: [],
);
super._initPeers = [];
}
@override
Widget build(BuildContext context) {
@ -210,13 +216,17 @@ class RecentPeerWidget extends BasePeerWidget {
}
class FavoritePeerWidget extends BasePeerWidget {
FavoritePeerWidget({Key? key}) : super(key: key) {
super._name = "favorite peer";
super._loadEvent = "load_fav_peers";
super._offstageFunc = (Peer _peer) => false;
super._peerCardWidgetFunc = (Peer peer) => FavoritePeerCard(peer: peer);
super._initPeers = [];
}
FavoritePeerWidget({Key? key})
: super(
key: key,
name: 'favorite peer',
loadEvent: 'load_fav_peers',
offstageFunc: (Peer peer) => false,
peerCardWidgetFunc: (Peer peer) => FavoritePeerCard(
peer: peer,
),
initPeers: [],
);
@override
Widget build(BuildContext context) {
@ -227,13 +237,17 @@ class FavoritePeerWidget extends BasePeerWidget {
}
class DiscoveredPeerWidget extends BasePeerWidget {
DiscoveredPeerWidget({Key? key}) : super(key: key) {
super._name = "discovered peer";
super._loadEvent = "load_lan_peers";
super._offstageFunc = (Peer _peer) => false;
super._peerCardWidgetFunc = (Peer peer) => DiscoveredPeerCard(peer: peer);
super._initPeers = [];
}
DiscoveredPeerWidget({Key? key})
: super(
key: key,
name: 'discovered peer',
loadEvent: 'load_lan_peers',
offstageFunc: (Peer peer) => false,
peerCardWidgetFunc: (Peer peer) => DiscoveredPeerCard(
peer: peer,
),
initPeers: [],
);
@override
Widget build(BuildContext context) {
@ -244,21 +258,26 @@ class DiscoveredPeerWidget extends BasePeerWidget {
}
class AddressBookPeerWidget extends BasePeerWidget {
AddressBookPeerWidget({Key? key}) : super(key: key) {
super._name = "address book peer";
super._offstageFunc =
(Peer peer) => !_hitTag(gFFI.abModel.selectedTags, peer.tags);
super._peerCardWidgetFunc = (Peer peer) => AddressBookPeerCard(peer: peer);
super._initPeers = _loadPeers();
}
AddressBookPeerWidget({Key? key})
: super(
key: key,
name: 'address book peer',
loadEvent: 'load_address_book_peers',
offstageFunc: (Peer peer) =>
!_hitTag(gFFI.abModel.selectedTags, peer.tags),
peerCardWidgetFunc: (Peer peer) => DiscoveredPeerCard(
peer: peer,
),
initPeers: _loadPeers(),
);
List<Peer> _loadPeers() {
static List<Peer> _loadPeers() {
return gFFI.abModel.peers.map((e) {
return Peer.fromJson(e['id'], e);
}).toList();
}
bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
if (selectedTags.isEmpty) {
return true;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,509 @@
import 'dart:core';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import './material_mod_popup_menu.dart' as mod_menu;
// https://stackoverflow.com/questions/68318314/flutter-popup-menu-inside-popup-menu
class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
const PopupMenuChildrenItem({
key,
this.height = kMinInteractiveDimension,
this.padding,
this.enable = true,
this.textStyle,
this.onTap,
this.position = mod_menu.PopupMenuPosition.overSide,
this.offset = Offset.zero,
required this.itemBuilder,
required this.child,
}) : super(key: key);
final mod_menu.PopupMenuPosition position;
final Offset offset;
final TextStyle? textStyle;
final EdgeInsets? padding;
final bool enable;
final void Function()? onTap;
final List<mod_menu.PopupMenuEntry<T>> Function(BuildContext) itemBuilder;
final Widget child;
@override
final double height;
@override
bool represents(T? value) => false;
@override
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() =>
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>();
}
class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
extends State<W> {
@protected
void handleTap(T value) {
widget.onTap?.call();
Navigator.pop<T>(context, value);
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
TextStyle style = widget.textStyle ??
popupMenuTheme.textStyle ??
theme.textTheme.subtitle1!;
return mod_menu.PopupMenuButton<T>(
enabled: widget.enable,
position: widget.position,
offset: widget.offset,
onSelected: handleTap,
itemBuilder: widget.itemBuilder,
padding: EdgeInsets.zero,
child: AnimatedDefaultTextStyle(
style: style,
duration: kThemeChangeDuration,
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: widget.height),
padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 16),
child: widget.child,
),
),
);
}
}
class MenuConfig {
// adapt to the screen height
static const fontSize = 14.0;
static const midPadding = 10.0;
static const iconScale = 0.8;
static const iconWidth = 12.0;
static const iconHeight = 12.0;
final double height;
final double dividerHeight;
final Color commonColor;
const MenuConfig(
{required this.commonColor,
this.height = kMinInteractiveDimension,
this.dividerHeight = 16.0});
}
abstract class MenuEntryBase<T> {
bool dismissOnClicked;
MenuEntryBase({this.dismissOnClicked = false});
List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf);
}
class MenuEntryDivider<T> extends MenuEntryBase<T> {
@override
List<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) {
return [
mod_menu.PopupMenuDivider(
height: conf.dividerHeight,
)
];
}
}
class MenuEntryRadioOption {
String text;
String value;
bool dismissOnClicked;
MenuEntryRadioOption(
{required this.text, required this.value, this.dismissOnClicked = false});
}
typedef RadioOptionsGetter = List<MenuEntryRadioOption> Function();
typedef RadioCurOptionGetter = Future<String> Function();
typedef RadioOptionSetter = Future<void> Function(
String oldValue, String newValue);
class MenuEntryRadioUtils<T> {}
class MenuEntryRadios<T> extends MenuEntryBase<T> {
final String text;
final RadioOptionsGetter optionsGetter;
final RadioCurOptionGetter curOptionGetter;
final RadioOptionSetter optionSetter;
final RxString _curOption = "".obs;
MenuEntryRadios(
{required this.text,
required this.optionsGetter,
required this.curOptionGetter,
required this.optionSetter,
dismissOnClicked = false})
: super(dismissOnClicked: dismissOnClicked) {
() async {
_curOption.value = await curOptionGetter();
}();
}
List<MenuEntryRadioOption> get options => optionsGetter();
RxString get curOption => _curOption;
setOption(String option) async {
await optionSetter(_curOption.value, option);
if (_curOption.value != option) {
final opt = await curOptionGetter();
if (_curOption.value != opt) {
_curOption.value = opt;
}
}
}
mod_menu.PopupMenuEntry<T> _buildMenuItem(
BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) {
return mod_menu.PopupMenuItem(
padding: EdgeInsets.zero,
height: conf.height,
child: TextButton(
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.height),
child: Row(
children: [
Text(
opt.text,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: SizedBox(
width: 20.0,
height: 20.0,
child: Obx(() => opt.value == curOption.value
? Icon(
Icons.check,
color: conf.commonColor,
)
: const SizedBox.shrink())),
)),
],
),
),
onPressed: () {
if (opt.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
}
setOption(opt.value);
},
),
);
}
@override
List<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) {
return options.map((opt) => _buildMenuItem(context, conf, opt)).toList();
}
}
class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
final String text;
final RadioOptionsGetter optionsGetter;
final RadioCurOptionGetter curOptionGetter;
final RadioOptionSetter optionSetter;
final RxString _curOption = "".obs;
MenuEntrySubRadios(
{required this.text,
required this.optionsGetter,
required this.curOptionGetter,
required this.optionSetter,
dismissOnClicked = false})
: super(dismissOnClicked: dismissOnClicked) {
() async {
_curOption.value = await curOptionGetter();
}();
}
List<MenuEntryRadioOption> get options => optionsGetter();
RxString get curOption => _curOption;
setOption(String option) async {
await optionSetter(_curOption.value, option);
if (_curOption.value != option) {
final opt = await curOptionGetter();
if (_curOption.value != opt) {
_curOption.value = opt;
}
}
}
mod_menu.PopupMenuEntry<T> _buildSecondMenu(
BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) {
return mod_menu.PopupMenuItem(
padding: EdgeInsets.zero,
height: conf.height,
child: TextButton(
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.height),
child: Row(
children: [
Text(
opt.text,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: SizedBox(
width: 20.0,
height: 20.0,
child: Obx(() => opt.value == curOption.value
? Icon(
Icons.check,
color: conf.commonColor,
)
: const SizedBox.shrink())),
)),
],
),
),
onPressed: () {
if (opt.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
}
setOption(opt.value);
},
),
);
}
@override
List<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) {
return [
PopupMenuChildrenItem(
padding: EdgeInsets.zero,
height: conf.height,
itemBuilder: (BuildContext context) =>
options.map((opt) => _buildSecondMenu(context, conf, opt)).toList(),
child: Row(children: [
const SizedBox(width: MenuConfig.midPadding),
Text(
text,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Icon(
Icons.keyboard_arrow_right,
color: conf.commonColor,
),
))
]),
)
];
}
}
typedef SwitchGetter = Future<bool> Function();
typedef SwitchSetter = Future<void> Function(bool);
abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
final String text;
MenuEntrySwitchBase({required this.text, required dismissOnClicked})
: super(dismissOnClicked: dismissOnClicked);
RxBool get curOption;
Future<void> setOption(bool option);
@override
List<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) {
return [
mod_menu.PopupMenuItem(
padding: EdgeInsets.zero,
height: conf.height,
child: TextButton(
child: Container(
alignment: AlignmentDirectional.centerStart,
height: conf.height,
child: Row(children: [
// const SizedBox(width: MenuConfig.midPadding),
Text(
text,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Obx(() => Switch(
value: curOption.value,
onChanged: (v) {
if (super.dismissOnClicked &&
Navigator.canPop(context)) {
Navigator.pop(context);
}
setOption(v);
},
)),
))
])),
onPressed: () {
if (super.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
}
setOption(!curOption.value);
},
),
)
];
}
}
class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
final SwitchGetter getter;
final SwitchSetter setter;
final RxBool _curOption = false.obs;
MenuEntrySwitch(
{required String text,
required this.getter,
required this.setter,
dismissOnClicked = false})
: super(text: text, dismissOnClicked: dismissOnClicked) {
() async {
_curOption.value = await getter();
}();
}
@override
RxBool get curOption => _curOption;
@override
setOption(bool option) async {
await setter(option);
final opt = await getter();
if (_curOption.value != opt) {
_curOption.value = opt;
}
}
}
typedef Switch2Getter = RxBool Function();
typedef Switch2Setter = Future<void> Function(bool);
class MenuEntrySwitch2<T> extends MenuEntrySwitchBase<T> {
final Switch2Getter getter;
final SwitchSetter setter;
MenuEntrySwitch2(
{required String text,
required this.getter,
required this.setter,
dismissOnClicked = false})
: super(text: text, dismissOnClicked: dismissOnClicked);
@override
RxBool get curOption => getter();
@override
setOption(bool option) async {
await setter(option);
}
}
class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
final String text;
final List<MenuEntryBase<T>> entries;
MenuEntrySubMenu({required this.text, required this.entries});
@override
List<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) {
return [
PopupMenuChildrenItem(
height: conf.height,
padding: EdgeInsets.zero,
position: mod_menu.PopupMenuPosition.overSide,
itemBuilder: (BuildContext context) => entries
.map((entry) => entry.build(context, conf))
.expand((i) => i)
.toList(),
child: Row(children: [
const SizedBox(width: MenuConfig.midPadding),
Text(
text,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Icon(
Icons.keyboard_arrow_right,
color: conf.commonColor,
),
))
]),
)
];
}
}
class MenuEntryButton<T> extends MenuEntryBase<T> {
final Widget Function(TextStyle? style) childBuilder;
Function() proc;
MenuEntryButton(
{required this.childBuilder,
required this.proc,
dismissOnClicked = false})
: super(dismissOnClicked: dismissOnClicked);
@override
List<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) {
return [
mod_menu.PopupMenuItem(
padding: EdgeInsets.zero,
height: conf.height,
child: TextButton(
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.height),
child: childBuilder(
const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
)),
onPressed: () {
if (super.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
}
proc();
},
),
)
];
}
}

View File

@ -0,0 +1,619 @@
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:rxdart/rxdart.dart' as rxdart;
import '../../common.dart';
import '../../mobile/widgets/dialog.dart';
import '../../mobile/widgets/overlay.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import './popup_menu.dart';
import './material_mod_popup_menu.dart' as mod_menu;
class _MenubarTheme {
static const Color commonColor = MyTheme.accent;
// kMinInteractiveDimension
static const double height = 25.0;
static const double dividerHeight = 12.0;
}
class RemoteMenubar extends StatefulWidget {
final String id;
final FFI ffi;
const RemoteMenubar({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
@override
State<RemoteMenubar> createState() => _RemoteMenubarState();
}
class _RemoteMenubarState extends State<RemoteMenubar> {
final RxBool _show = false.obs;
final Rx<Color> _hideColor = Colors.white12.obs;
bool get isFullscreen => Get.find<RxBool>(tag: 'fullscreen').isTrue;
void setFullscreen(bool v) {
Get.find<RxBool>(tag: 'fullscreen').value = v;
}
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topCenter,
child: Obx(
() => _show.value ? _buildMenubar(context) : _buildShowHide(context)),
);
}
Widget _buildShowHide(BuildContext context) {
return Obx(() => Tooltip(
message: translate(_show.value ? "Hide Menubar" : "Show Menubar"),
child: SizedBox(
width: 100,
height: 5,
child: TextButton(
onHover: (bool v) {
_hideColor.value = v ? Colors.white60 : Colors.white24;
},
onPressed: () {
_show.value = !_show.value;
},
child: Obx(() => Container(
color: _hideColor.value,
)))),
));
}
Widget _buildMenubar(BuildContext context) {
final List<Widget> menubarItems = [];
if (!isWebDesktop) {
menubarItems.add(_buildFullscreen(context));
if (widget.ffi.ffiModel.isPeerAndroid) {
menubarItems.add(IconButton(
tooltip: translate('Mobile Actions'),
color: _MenubarTheme.commonColor,
icon: const Icon(Icons.build),
onPressed: () {
if (mobileActionsOverlayEntry == null) {
showMobileActionsOverlay();
} else {
hideMobileActionsOverlay();
}
},
));
}
}
menubarItems.add(_buildMonitor(context));
menubarItems.add(_buildControl(context));
menubarItems.add(_buildDisplay(context));
if (!isWeb) {
menubarItems.add(_buildChat(context));
}
menubarItems.add(_buildClose(context));
return PopupMenuTheme(
data: const PopupMenuThemeData(
textStyle: TextStyle(color: _MenubarTheme.commonColor)),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Container(
color: Colors.white,
child: Row(
mainAxisSize: MainAxisSize.min,
children: menubarItems,
)),
_buildShowHide(context),
]));
}
Widget _buildFullscreen(BuildContext context) {
return IconButton(
tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'),
onPressed: () {
setFullscreen(!isFullscreen);
},
icon: Obx(() => isFullscreen
? const Icon(
Icons.fullscreen_exit,
color: _MenubarTheme.commonColor,
)
: const Icon(
Icons.fullscreen,
color: _MenubarTheme.commonColor,
)),
);
}
Widget _buildChat(BuildContext context) {
return IconButton(
tooltip: translate('Chat'),
onPressed: () {
widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
widget.ffi.chatModel.toggleChatOverlay();
},
icon: const Icon(
Icons.message,
color: _MenubarTheme.commonColor,
),
);
}
Widget _buildMonitor(BuildContext context) {
final pi = widget.ffi.ffiModel.pi;
return mod_menu.PopupMenuButton(
tooltip: translate('Select Monitor'),
padding: EdgeInsets.zero,
position: mod_menu.PopupMenuPosition.under,
icon: Stack(
alignment: Alignment.center,
children: [
const Icon(
Icons.personal_video,
color: _MenubarTheme.commonColor,
),
Padding(
padding: const EdgeInsets.only(bottom: 3.9),
child: Obx(() {
RxInt display = CurrentDisplayState.find(widget.id);
return Text(
"${display.value + 1}/${pi.displays.length}",
style: const TextStyle(
color: _MenubarTheme.commonColor, fontSize: 8),
);
}),
)
],
),
itemBuilder: (BuildContext context) {
final List<Widget> rowChildren = [];
for (int i = 0; i < pi.displays.length; i++) {
rowChildren.add(
Stack(
alignment: Alignment.center,
children: [
const Icon(
Icons.personal_video,
color: _MenubarTheme.commonColor,
),
TextButton(
child: Container(
alignment: AlignmentDirectional.center,
constraints:
const BoxConstraints(minHeight: _MenubarTheme.height),
child: Padding(
padding: const EdgeInsets.only(bottom: 2.5),
child: Text(
(i + 1).toString(),
style:
const TextStyle(color: _MenubarTheme.commonColor),
),
)),
onPressed: () {
RxInt display = CurrentDisplayState.find(widget.id);
if (display.value != i) {
bind.sessionSwitchDisplay(id: widget.id, value: i);
pi.currentDisplay = i;
display.value = i;
}
},
)
],
),
);
}
return <mod_menu.PopupMenuEntry<String>>[
mod_menu.PopupMenuItem<String>(
height: _MenubarTheme.height,
padding: EdgeInsets.zero,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: rowChildren),
)
];
},
);
}
Widget _buildControl(BuildContext context) {
return mod_menu.PopupMenuButton(
padding: EdgeInsets.zero,
icon: const Icon(
Icons.bolt,
color: _MenubarTheme.commonColor,
),
tooltip: translate('Control Actions'),
position: mod_menu.PopupMenuPosition.under,
itemBuilder: (BuildContext context) => _getControlMenu()
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: _MenubarTheme.commonColor,
height: _MenubarTheme.height,
dividerHeight: _MenubarTheme.dividerHeight,
)))
.expand((i) => i)
.toList(),
);
}
Widget _buildDisplay(BuildContext context) {
return mod_menu.PopupMenuButton(
padding: EdgeInsets.zero,
icon: const Icon(
Icons.tv,
color: _MenubarTheme.commonColor,
),
tooltip: translate('Display Settings'),
position: mod_menu.PopupMenuPosition.under,
onSelected: (String item) {},
itemBuilder: (BuildContext context) => _getDisplayMenu()
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: _MenubarTheme.commonColor,
height: _MenubarTheme.height,
dividerHeight: _MenubarTheme.dividerHeight,
)))
.expand((i) => i)
.toList(),
);
}
Widget _buildClose(BuildContext context) {
return IconButton(
tooltip: translate('Close'),
onPressed: () {
clientClose(widget.ffi.dialogManager);
},
icon: const Icon(
Icons.close,
color: _MenubarTheme.commonColor,
),
);
}
List<MenuEntryBase<String>> _getControlMenu() {
final pi = widget.ffi.ffiModel.pi;
final perms = widget.ffi.ffiModel.permissions;
final List<MenuEntryBase<String>> displayMenu = [];
if (pi.version.isNotEmpty) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Refresh'),
style: style,
),
proc: () {
bind.sessionRefresh(id: widget.id);
},
dismissOnClicked: true,
));
}
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('OS Password'),
style: style,
),
proc: () {
showSetOSPassword(widget.id, false, widget.ffi.dialogManager);
},
dismissOnClicked: true,
));
if (!isWebDesktop) {
if (perms['keyboard'] != false && perms['clipboard'] != false) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Paste'),
style: style,
),
proc: () {
() async {
ClipboardData? data =
await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) {
bind.sessionInputString(id: widget.id, value: data.text ?? "");
}
}();
},
dismissOnClicked: true,
));
}
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Reset canvas'),
style: style,
),
proc: () {
widget.ffi.cursorModel.reset();
},
dismissOnClicked: true,
));
}
if (perms['keyboard'] != false) {
if (pi.platform == 'Linux' || pi.sasEnabled) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
'${translate("Insert")} Ctrl + Alt + Del',
style: style,
),
proc: () {
bind.sessionCtrlAltDel(id: widget.id);
},
dismissOnClicked: true,
));
}
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Insert Lock'),
style: style,
),
proc: () {
bind.sessionLockScreen(id: widget.id);
},
dismissOnClicked: true,
));
if (pi.platform == 'Windows') {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Obx(() => Text(
translate(
'${BlockInputState.find(widget.id).value ? "Unb" : "B"}lock user input'),
style: style,
)),
proc: () {
RxBool blockInput = BlockInputState.find(widget.id);
bind.sessionToggleOption(
id: widget.id,
value: '${blockInput.value ? "un" : ""}block-input');
blockInput.value = !blockInput.value;
},
dismissOnClicked: true,
));
}
}
if (gFFI.ffiModel.permissions["restart"] != false &&
(pi.platform == "Linux" ||
pi.platform == "Windows" ||
pi.platform == "Mac OS")) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Restart Remote Device'),
style: style,
),
proc: () {
showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager);
},
dismissOnClicked: true,
));
}
return displayMenu;
}
List<MenuEntryBase<String>> _getDisplayMenu() {
final displayMenu = [
MenuEntryRadios<String>(
text: translate('Ratio'),
optionsGetter: () => [
MenuEntryRadioOption(
text: translate('Scale original'), value: 'original'),
MenuEntryRadioOption(
text: translate('Scale adaptive'), value: 'adaptive'),
],
curOptionGetter: () async {
return await bind.sessionGetOption(
id: widget.id, arg: 'view-style') ??
'adaptive';
},
optionSetter: (String oldValue, String newValue) async {
await bind.sessionPeerOption(
id: widget.id, name: "view-style", value: newValue);
widget.ffi.canvasModel.updateViewStyle();
}),
MenuEntryDivider<String>(),
MenuEntryRadios<String>(
text: translate('Scroll Style'),
optionsGetter: () => [
MenuEntryRadioOption(
text: translate('ScrollAuto'), value: 'scrollauto'),
MenuEntryRadioOption(
text: translate('Scrollbar'), value: 'scrollbar'),
],
curOptionGetter: () async {
return await bind.sessionGetOption(
id: widget.id, arg: 'scroll-style') ??
'';
},
optionSetter: (String oldValue, String newValue) async {
await bind.sessionPeerOption(
id: widget.id, name: "scroll-style", value: newValue);
widget.ffi.canvasModel.updateScrollStyle();
}),
MenuEntryDivider<String>(),
MenuEntryRadios<String>(
text: translate('Image Quality'),
optionsGetter: () => [
MenuEntryRadioOption(
text: translate('Good image quality'), value: 'best'),
MenuEntryRadioOption(
text: translate('Balanced'), value: 'balanced'),
MenuEntryRadioOption(
text: translate('Optimize reaction time'), value: 'low'),
MenuEntryRadioOption(
text: translate('Custom'),
value: 'custom',
dismissOnClicked: true),
],
curOptionGetter: () async {
String quality =
await bind.sessionGetImageQuality(id: widget.id) ?? 'balanced';
if (quality == '') quality = 'balanced';
return quality;
},
optionSetter: (String oldValue, String newValue) async {
if (oldValue != newValue) {
await bind.sessionSetImageQuality(id: widget.id, value: newValue);
}
if (newValue == 'custom') {
final btnCancel = msgBoxButton(translate('Close'), () {
widget.ffi.dialogManager.dismissAll();
});
final quality =
await bind.sessionGetCustomImageQuality(id: widget.id);
final double initValue = quality != null && quality.isNotEmpty
? quality[0].toDouble()
: 50.0;
final RxDouble sliderValue = RxDouble(initValue);
final rxReplay = rxdart.ReplaySubject<double>();
rxReplay
.throttleTime(const Duration(milliseconds: 1000),
trailing: true, leading: false)
.listen((double v) {
() async {
await bind.sessionSetCustomImageQuality(
id: widget.id, value: v.toInt());
}();
});
final slider = Obx(() {
return Slider(
value: sliderValue.value,
max: 100,
divisions: 100,
label: sliderValue.value.round().toString(),
onChanged: (double value) {
sliderValue.value = value;
rxReplay.add(value);
},
);
});
msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality',
slider, [btnCancel]);
}
}),
MenuEntryDivider<String>(),
MenuEntrySwitch<String>(
text: translate('Show remote cursor'),
getter: () async {
return bind.sessionGetToggleOptionSync(
id: widget.id, arg: 'show-remote-cursor');
},
setter: (bool v) async {
await bind.sessionToggleOption(
id: widget.id, value: 'show-remote-cursor');
}),
MenuEntrySwitch<String>(
text: translate('Show quality monitor'),
getter: () async {
return bind.sessionGetToggleOptionSync(
id: widget.id, arg: 'show-quality-monitor');
},
setter: (bool v) async {
await bind.sessionToggleOption(
id: widget.id, value: 'show-quality-monitor');
widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
}),
];
final perms = widget.ffi.ffiModel.permissions;
final pi = widget.ffi.ffiModel.pi;
if (perms['audio'] != false) {
displayMenu.add(_createSwitchMenuEntry('Mute', 'disable-audio'));
}
if (perms['keyboard'] != false) {
if (perms['clipboard'] != false) {
displayMenu.add(
_createSwitchMenuEntry('Disable clipboard', 'disable-clipboard'));
}
displayMenu.add(_createSwitchMenuEntry(
'Lock after session end', 'lock-after-session-end'));
if (pi.platform == 'Windows') {
displayMenu.add(MenuEntrySwitch2<String>(
text: translate('Privacy mode'),
getter: () {
return PrivacyModeState.find(widget.id);
},
setter: (bool v) async {
Navigator.pop(context);
await bind.sessionToggleOption(
id: widget.id, value: 'privacy-mode');
}));
}
}
return displayMenu;
}
MenuEntrySwitch<String> _createSwitchMenuEntry(String text, String option) {
return MenuEntrySwitch<String>(
text: translate(text),
getter: () async {
return bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
},
setter: (bool v) async {
await bind.sessionToggleOption(id: widget.id, value: option);
});
}
}
void showSetOSPassword(
String id, bool login, OverlayDialogManager dialogManager) async {
final controller = TextEditingController();
var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? "";
var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != "";
controller.text = password;
dialogManager.show((setState, close) {
return CustomAlertDialog(
title: Text(translate('OS Password')),
content: Column(mainAxisSize: MainAxisSize.min, children: [
PasswordWidget(controller: controller),
CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Text(
translate('Auto Login'),
),
value: autoLogin,
onChanged: (v) {
if (v == null) return;
setState(() => autoLogin = v);
},
),
]),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: () {
close();
},
child: Text(translate('Cancel')),
),
TextButton(
style: flatButtonStyle,
onPressed: () {
var text = controller.text.trim();
bind.sessionPeerOption(id: id, name: "os-password", value: text);
bind.sessionPeerOption(
id: id, name: "auto-login", value: autoLogin ? 'Y' : '');
if (text != "" && login) {
bind.sessionInputOsPassword(id: id, value: text);
}
close();
},
child: Text(translate('OK')),
),
]);
});
}

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'dart:async';
import 'dart:math';
import 'package:desktop_multi_window/desktop_multi_window.dart';
@ -5,9 +7,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
import 'package:scroll_pos/scroll_pos.dart';
import 'package:window_manager/window_manager.dart';
import '../../utils/multi_window_manager.dart';
@ -33,6 +36,15 @@ class TabInfo {
required this.page});
}
enum DesktopTabType {
main,
cm,
remoteScreen,
fileTransfer,
portForward,
rdp,
}
class DesktopTabState {
final List<TabInfo> tabs = [];
final ScrollPosController scrollController =
@ -63,6 +75,7 @@ class DesktopTabController {
state.update((val) {
val!.tabs.add(tab);
});
state.value.scrollController.itemCount = state.value.tabs.length;
toIndex = state.value.tabs.length - 1;
assert(toIndex >= 0);
}
@ -95,8 +108,16 @@ class DesktopTabController {
void jumpTo(int index) {
state.update((val) {
val!.selected = index;
Future.delayed(Duration.zero, (() {
if (val.pageController.hasClients) {
val.pageController.jumpToPage(index);
}
if (val.scrollController.hasClients &&
val.scrollController.canScroll &&
val.scrollController.itemCount >= index) {
val.scrollController.scrollToItem(index, center: true, animate: true);
}
}));
});
onSelected?.call(index);
}
@ -113,11 +134,27 @@ class DesktopTabController {
remove(state.value.selected);
}
}
void clear() {
state.value.tabs.clear();
state.refresh();
}
}
class TabThemeConf {
double iconSize;
TarBarTheme theme;
TabThemeConf({required this.iconSize, required this.theme});
}
typedef TabBuilder = Widget Function(
String key, Widget icon, Widget label, TabThemeConf themeConf);
typedef LabelGetter = Rx<String> Function(String key);
class DesktopTab extends StatelessWidget {
final Function(String)? onTabClose;
final TarBarTheme theme;
final DesktopTabType tabType;
final bool isMainWindow;
final bool showTabBar;
final bool showLogo;
@ -127,13 +164,16 @@ class DesktopTab extends StatelessWidget {
final bool showClose;
final Widget Function(Widget pageView)? pageViewBuilder;
final Widget? tail;
final VoidCallback? onClose;
final TabBuilder? tabBuilder;
final LabelGetter? labelGetter;
final DesktopTabController controller;
late final state = controller.state;
Rx<DesktopTabState> get state => controller.state;
DesktopTab(
{required this.controller,
required this.isMainWindow,
const DesktopTab({
required this.controller,
required this.tabType,
this.theme = const TarBarTheme.light(),
this.onTabClose,
this.showTabBar = true,
@ -143,7 +183,12 @@ class DesktopTab extends StatelessWidget {
this.showMaximize = true,
this.showClose = true,
this.pageViewBuilder,
this.tail});
this.tail,
this.onClose,
this.tabBuilder,
this.labelGetter,
}) : isMainWindow =
tabType == DesktopTabType.main || tabType == DesktopTabType.cm;
@override
Widget build(BuildContext context) {
@ -172,11 +217,48 @@ class DesktopTab extends StatelessWidget {
]);
}
Widget _buildBlock({required Widget child}) {
if (tabType != DesktopTabType.main) {
return child;
}
var block = false.obs;
return Obx(() => MouseRegion(
onEnter: (_) async {
if (!option2bool(
'allow-remote-config-modification',
await bind.mainGetOption(
key: 'allow-remote-config-modification'))) {
var time0 = DateTime.now().millisecondsSinceEpoch;
await bind.mainCheckMouseTime();
Timer(const Duration(milliseconds: 120), () async {
var d = time0 - await bind.mainGetMouseTime();
if (d < 120) {
block.value = true;
}
});
}
},
onExit: (_) => block.value = false,
child: Stack(
children: [
child,
Offstage(
offstage: !block.value,
child: Container(
color: Colors.black.withOpacity(0.5),
)),
],
),
));
}
Widget _buildPageView() {
return Obx(() => PageView(
return _buildBlock(
child: Obx(() => PageView(
controller: state.value.pageController,
children:
state.value.tabs.map((tab) => tab.page).toList(growable: false)));
children: state.value.tabs
.map((tab) => tab.page)
.toList(growable: false))));
}
Widget _buildBar() {
@ -185,6 +267,11 @@ class DesktopTab extends StatelessWidget {
Expanded(
child: Row(
children: [
Offstage(
offstage: !Platform.isMacOS,
child: const SizedBox(
width: 78,
)),
Row(children: [
Offstage(
offstage: !showLogo,
@ -217,6 +304,8 @@ class DesktopTab extends StatelessWidget {
controller: controller,
onTabClose: onTabClose,
theme: theme,
tabBuilder: tabBuilder,
labelGetter: labelGetter,
)),
),
],
@ -229,6 +318,7 @@ class DesktopTab extends StatelessWidget {
showMinimize: showMinimize,
showMaximize: showMaximize,
showClose: showClose,
onClose: onClose,
)
],
);
@ -242,6 +332,7 @@ class WindowActionPanel extends StatelessWidget {
final bool showMinimize;
final bool showMaximize;
final bool showClose;
final VoidCallback? onClose;
const WindowActionPanel(
{Key? key,
@ -249,7 +340,8 @@ class WindowActionPanel extends StatelessWidget {
required this.theme,
this.showMinimize = true,
this.showMaximize = true,
this.showClose = true})
this.showClose = true,
this.onClose})
: super(key: key);
@override
@ -323,8 +415,12 @@ class WindowActionPanel extends StatelessWidget {
if (mainTab) {
windowManager.close();
} else {
WindowController.fromWindowId(windowId!).close();
// only hide for multi window, not close
Future.delayed(Duration.zero, () {
WindowController.fromWindowId(windowId!).hide();
});
}
onClose?.call();
},
is_close: true,
)),
@ -336,13 +432,20 @@ class WindowActionPanel extends StatelessWidget {
// ignore: must_be_immutable
class _ListView extends StatelessWidget {
final DesktopTabController controller;
late final Rx<DesktopTabState> state;
final Function(String key)? onTabClose;
final TarBarTheme theme;
final TabBuilder? tabBuilder;
final LabelGetter? labelGetter;
Rx<DesktopTabState> get state => controller.state;
_ListView(
{required this.controller, required this.onTabClose, required this.theme})
: this.state = controller.state;
{required this.controller,
required this.onTabClose,
required this.theme,
this.tabBuilder,
this.labelGetter});
@override
Widget build(BuildContext context) {
@ -356,7 +459,9 @@ class _ListView extends StatelessWidget {
final tab = e.value;
return _Tab(
index: index,
label: tab.label,
label: labelGetter == null
? Rx<String>(tab.label)
: labelGetter!(tab.label),
selectedIcon: tab.selectedIcon,
unselectedIcon: tab.unselectedIcon,
closable: tab.closable,
@ -364,22 +469,33 @@ class _ListView extends StatelessWidget {
onClose: () => controller.remove(index),
onSelected: () => controller.jumpTo(index),
theme: theme,
tabBuilder: tabBuilder == null
? null
: (Widget icon, Widget labelWidget, TabThemeConf themeConf) {
return tabBuilder!(
tab.label,
icon,
labelWidget,
themeConf,
);
},
);
}).toList()));
}
}
class _Tab extends StatelessWidget {
class _Tab extends StatefulWidget {
late final int index;
late final String label;
late final Rx<String> label;
late final IconData? selectedIcon;
late final IconData? unselectedIcon;
late final bool closable;
late final int selected;
late final Function() onClose;
late final Function() onSelected;
final RxBool _hover = false.obs;
late final TarBarTheme theme;
final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)?
tabBuilder;
_Tab(
{Key? key,
@ -387,6 +503,7 @@ class _Tab extends StatelessWidget {
required this.label,
this.selectedIcon,
this.unselectedIcon,
this.tabBuilder,
required this.closable,
required this.selected,
required this.onClose,
@ -394,61 +511,87 @@ class _Tab extends StatelessWidget {
required this.theme})
: super(key: key);
@override
State<_Tab> createState() => _TabState();
}
class _TabState extends State<_Tab> with RestorationMixin {
final RestorableBool restoreHover = RestorableBool(false);
Widget _buildTabContent() {
bool showIcon =
widget.selectedIcon != null && widget.unselectedIcon != null;
bool isSelected = widget.index == widget.selected;
final icon = Offstage(
offstage: !showIcon,
child: Icon(
isSelected ? widget.selectedIcon : widget.unselectedIcon,
size: _kIconSize,
color: isSelected
? widget.theme.selectedtabIconColor
: widget.theme.unSelectedtabIconColor,
).paddingOnly(right: 5));
final labelWidget = Obx(() {
return Text(
translate(widget.label.value),
textAlign: TextAlign.center,
style: TextStyle(
color: isSelected
? widget.theme.selectedTextColor
: widget.theme.unSelectedTextColor),
);
});
if (widget.tabBuilder == null) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
labelWidget,
],
);
} else {
return widget.tabBuilder!(icon, labelWidget,
TabThemeConf(iconSize: _kIconSize, theme: widget.theme));
}
}
@override
Widget build(BuildContext context) {
bool show_icon = selectedIcon != null && unselectedIcon != null;
bool is_selected = index == selected;
bool show_divider = index != selected - 1 && index != selected;
bool isSelected = widget.index == widget.selected;
bool showDivider =
widget.index != widget.selected - 1 && widget.index != widget.selected;
RxBool hover = restoreHover.value.obs;
return Ink(
child: InkWell(
onHover: (hover) => _hover.value = hover,
onTap: () => onSelected(),
onHover: (value) {
hover.value = value;
restoreHover.value = value;
},
onTap: () => widget.onSelected(),
child: Row(
children: [
Container(
SizedBox(
height: _kTabBarHeight,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Offstage(
offstage: !show_icon,
child: Icon(
is_selected ? selectedIcon : unselectedIcon,
size: _kIconSize,
color: is_selected
? theme.selectedtabIconColor
: theme.unSelectedtabIconColor,
).paddingOnly(right: 5)),
Text(
translate(label),
textAlign: TextAlign.center,
style: TextStyle(
color: is_selected
? theme.selectedTextColor
: theme.unSelectedTextColor),
),
],
),
Offstage(
offstage: !closable,
child: Obx((() => _CloseButton(
visiable: _hover.value,
tabSelected: is_selected,
onClose: () => onClose(),
theme: theme,
))),
)
_buildTabContent(),
Obx((() => _CloseButton(
visiable: hover.value && widget.closable,
tabSelected: isSelected,
onClose: () => widget.onClose(),
theme: widget.theme,
)))
])).paddingSymmetric(horizontal: 10),
Offstage(
offstage: !show_divider,
offstage: !showDivider,
child: VerticalDivider(
width: 1,
indent: _kDividerIndent,
endIndent: _kDividerIndent,
color: theme.dividerColor,
color: widget.theme.dividerColor,
thickness: 1,
),
)
@ -457,6 +600,14 @@ class _Tab extends StatelessWidget {
),
);
}
@override
String? get restorationId => "_Tab${widget.label.value}";
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(restoreHover, 'restoreHover');
}
}
class _CloseButton extends StatelessWidget {
@ -480,7 +631,7 @@ class _CloseButton extends StatelessWidget {
child: Offstage(
offstage: !visiable,
child: InkWell(
customBorder: RoundedRectangleBorder(),
customBorder: const RoundedRectangleBorder(),
onTap: () => onClose(),
child: Icon(
Icons.close,

File diff suppressed because it is too large Load Diff

View File

@ -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/server_page.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/utils/multi_window_manager.dart';
import 'package:get/get.dart';
@ -47,6 +48,9 @@ Future<Null> main(List<String> args) async {
case WindowType.FileTransfer:
runFileTransferScreen(argument);
break;
case WindowType.PortForward:
runPortForwardScreen(argument);
break;
default:
break;
}
@ -76,14 +80,9 @@ Future<void> initEnv(String appType) async {
}
void runMainApp(bool startService) async {
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(1280, 720));
await Future.wait([
initEnv(kAppTypeMain),
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
})
]);
await initEnv(kAppTypeMain);
// trigger connection status updater
await bind.mainCheckConnectStatus();
if (startService) {
// await windowManager.ensureInitialized();
// disable tray
@ -91,6 +90,13 @@ void runMainApp(bool startService) async {
gFFI.serverModel.startService();
}
runApp(App());
// set window option
WindowOptions windowOptions =
getHiddenTitleBarWindowOptions(const Size(1280, 720));
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
}
void runMobileApp() async {
@ -133,6 +139,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 {
// initialize window
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(300, 400));
@ -182,7 +205,7 @@ class App extends StatelessWidget {
title: 'RustDesk',
theme: getCurrentTheme(),
home: isDesktop
? DesktopTabPage()
? const DesktopTabPage()
: !isAndroid
? WebHomePage()
: HomePage(),
@ -190,8 +213,13 @@ class App extends StatelessWidget {
// FirebaseAnalyticsObserver(analytics: analytics),
],
builder: isAndroid
? (_, child) => AccessibilityListener(
child: child,
? (context, child) => AccessibilityListener(
child: MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: 1.0,
),
child: child ?? Container(),
),
)
: _keepScaleBuilder(),
),

View File

@ -7,6 +7,7 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/generated_bridge.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart';
@ -17,6 +18,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:tuple/tuple.dart';
import '../common.dart';
import '../common/shared_state.dart';
import '../mobile/widgets/dialog.dart';
import '../mobile/widgets/overlay.dart';
import 'peer_model.dart';
@ -96,25 +98,26 @@ class FfiModel with ChangeNotifier {
clearPermissions();
}
void setConnectionType(bool secure, bool direct) {
void setConnectionType(String peerId, bool secure, bool direct) {
_secure = secure;
_direct = direct;
try {
var connectionType = ConnectionTypeState.find(peerId);
connectionType.setSecure(secure);
connectionType.setDirect(direct);
} catch (e) {
//
}
}
Image? getConnectionImage() {
String? icon;
if (secure == true && direct == true) {
icon = 'secure';
} else if (secure == false && direct == true) {
icon = 'insecure';
} else if (secure == false && direct == false) {
icon = 'insecure_relay';
} else if (secure == true && direct == false) {
icon = 'secure_relay';
if (secure == null || direct == null) {
return null;
} else {
final icon =
'${secure == true ? "secure" : "insecure"}${direct == true ? "" : "_relay"}';
return Image.asset('assets/$icon.png', width: 48, height: 48);
}
return icon == null
? null
: Image.asset('assets/$icon.png', width: 48, height: 48);
}
void clearPermissions() {
@ -130,7 +133,8 @@ class FfiModel with ChangeNotifier {
} else if (name == 'peer_info') {
handlePeerInfo(evt, peerId);
} else if (name == 'connection_ready') {
setConnectionType(evt['secure'] == 'true', evt['direct'] == 'true');
setConnectionType(
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
} else if (name == 'switch_display') {
handleSwitchDisplay(evt);
} else if (name == 'cursor_data') {
@ -172,9 +176,9 @@ class FfiModel with ChangeNotifier {
} else if (name == 'update_quality_status') {
parent.target?.qualityMonitorModel.updateQualityStatus(evt);
} else if (name == 'update_block_input_state') {
updateBlockInputState(evt);
updateBlockInputState(evt, peerId);
} else if (name == 'update_privacy_mode') {
updatePrivacyMode(evt);
updatePrivacyMode(evt, peerId);
}
};
}
@ -189,7 +193,7 @@ class FfiModel with ChangeNotifier {
handlePeerInfo(evt, peerId);
} else if (name == 'connection_ready') {
parent.target?.ffiModel.setConnectionType(
evt['secure'] == 'true', evt['direct'] == 'true');
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
} else if (name == 'switch_display') {
handleSwitchDisplay(evt);
} else if (name == 'cursor_data') {
@ -231,9 +235,9 @@ class FfiModel with ChangeNotifier {
} else if (name == 'update_quality_status') {
parent.target?.qualityMonitorModel.updateQualityStatus(evt);
} else if (name == 'update_block_input_state') {
updateBlockInputState(evt);
updateBlockInputState(evt, peerId);
} else if (name == 'update_privacy_mode') {
updatePrivacyMode(evt);
updatePrivacyMode(evt, peerId);
}
};
platformFFI.setEventCallback(cb);
@ -297,6 +301,9 @@ class FfiModel with ChangeNotifier {
/// Handle the peer info event based on [evt].
void handlePeerInfo(Map<String, dynamic> evt, String peerId) async {
// recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
bind.mainLoadRecentPeers();
parent.target?.dialogManager.dismissAll();
_pi.version = evt['version'];
_pi.username = evt['username'];
@ -305,6 +312,12 @@ class FfiModel with ChangeNotifier {
_pi.sasEnabled = evt['sas_enabled'] == "true";
_pi.currentDisplay = int.parse(evt['current_display']);
try {
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
} catch (e) {
//
}
if (isPeerAndroid) {
_touchMode = true;
if (parent.target?.ffiModel.permissions['keyboard'] != false) {
@ -316,6 +329,7 @@ class FfiModel with ChangeNotifier {
}
if (evt['is_file_transfer'] == "true") {
// TODO is file transfer
parent.target?.fileModel.onReady();
} else {
_pi.displays = [];
@ -343,13 +357,24 @@ class FfiModel with ChangeNotifier {
notifyListeners();
}
updateBlockInputState(Map<String, dynamic> evt) {
updateBlockInputState(Map<String, dynamic> evt, String peerId) {
_inputBlocked = evt['input_state'] == 'on';
notifyListeners();
try {
BlockInputState.find(peerId).value = evt['input_state'] == 'on';
} catch (e) {
//
}
}
updatePrivacyMode(Map<String, dynamic> evt) {
updatePrivacyMode(Map<String, dynamic> evt, String peerId) {
notifyListeners();
try {
PrivacyModeState.find(peerId).value =
bind.sessionGetToggleOptionSync(id: peerId, arg: 'privacy-mode');
} catch (e) {
//
}
}
}
@ -476,39 +501,11 @@ class CanvasModel with ChangeNotifier {
return;
}
final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720);
final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280);
// Closure to perform shrink operation.
final shrinkOp = () {
final s = s1 < s2 ? s1 : s2;
if (s < 1) {
_scale = s;
}
};
// Closure to perform stretch operation.
final stretchOp = () {
final s = s1 < s2 ? s1 : s2;
if (s > 1) {
_scale = s;
}
};
// Closure to perform default operation(set the scale to 1.0).
final defaultOp = () {
_scale = 1.0;
};
// // On desktop, shrink is the default behavior.
// if (isDesktop) {
// shrinkOp();
// } else {
defaultOp();
// }
if (style == 'shrink') {
shrinkOp();
} else if (style == 'stretch') {
stretchOp();
if (style == 'adaptive') {
final s1 = size.width / getDisplayWidth();
final s2 = size.height / getDisplayHeight();
_scale = s1 < s2 ? s1 : s2;
}
_x = (size.width - getDisplayWidth() * _scale) / 2;
@ -536,11 +533,17 @@ class CanvasModel with ChangeNotifier {
}
int getDisplayWidth() {
return parent.target?.ffiModel.display.width ?? 1080;
final defaultWidth = (isDesktop || isWebDesktop)
? kDesktopDefaultDisplayWidth
: kMobileDefaultDisplayWidth;
return parent.target?.ffiModel.display.width ?? defaultWidth;
}
int getDisplayHeight() {
return parent.target?.ffiModel.display.height ?? 720;
final defaultHeight = (isDesktop || isWebDesktop)
? kDesktopDefaultDisplayHeight
: kMobileDefaultDisplayHeight;
return parent.target?.ffiModel.display.height ?? defaultHeight;
}
Size get size {
@ -556,9 +559,19 @@ class CanvasModel with ChangeNotifier {
var dxOffset = 0;
var dyOffset = 0;
if (dw > size.width) {
final X_debugNanOrInfinite = x - dw * (x / size.width) - _x;
if (X_debugNanOrInfinite.isInfinite || X_debugNanOrInfinite.isNaN) {
debugPrint(
'REMOVE ME ============================ X_debugNanOrInfinite $x,$dw,$_scale,${size.width},$_x');
}
dxOffset = (x - dw * (x / size.width) - _x).toInt();
}
if (dh > size.height) {
final Y_debugNanOrInfinite = y - dh * (y / size.height) - _y;
if (Y_debugNanOrInfinite.isInfinite || Y_debugNanOrInfinite.isNaN) {
debugPrint(
'REMOVE ME ============================ Y_debugNanOrInfinite $y,$dh,$_scale,${size.height},$_y');
}
dyOffset = (y - dh * (y / size.height) - _y).toInt();
}
_x += dxOffset;
@ -926,16 +939,16 @@ class FFI {
late final QualityMonitorModel qualityMonitorModel; // session
FFI() {
this.imageModel = ImageModel(WeakReference(this));
this.ffiModel = FfiModel(WeakReference(this));
this.cursorModel = CursorModel(WeakReference(this));
this.canvasModel = CanvasModel(WeakReference(this));
this.serverModel = ServerModel(WeakReference(this)); // use global FFI
this.chatModel = ChatModel(WeakReference(this));
this.fileModel = FileModel(WeakReference(this));
this.abModel = AbModel(WeakReference(this));
this.userModel = UserModel(WeakReference(this));
this.qualityMonitorModel = QualityMonitorModel(WeakReference(this));
imageModel = ImageModel(WeakReference(this));
ffiModel = FfiModel(WeakReference(this));
cursorModel = CursorModel(WeakReference(this));
canvasModel = CanvasModel(WeakReference(this));
serverModel = ServerModel(WeakReference(this)); // use global FFI
chatModel = ChatModel(WeakReference(this));
fileModel = FileModel(WeakReference(this));
abModel = AbModel(WeakReference(this));
userModel = UserModel(WeakReference(this));
qualityMonitorModel = QualityMonitorModel(WeakReference(this));
}
/// Send a mouse tap event(down and up).
@ -983,7 +996,7 @@ class FFI {
// Raw Key
void inputRawKey(int keyCode, int scanCode, bool down){
debugPrint(scanCode.toString());
bind.sessionInputRawKey(id: id, keycode: keyCode, scancode: scanCode, down: down);
// bind.sessionInputRawKey(id: id, keycode: keyCode, scancode: scanCode, down: down);
}
/// Send key stroke event.
@ -1040,17 +1053,26 @@ class FFI {
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,
{bool isFileTransfer = false, double tabBarHeight = 0.0}) {
if (!isFileTransfer) {
{bool isFileTransfer = false,
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();
canvasModel.id = id;
imageModel._id = id;
cursorModel.id = id;
}
id = isFileTransfer ? 'ft_${id}' : id;
final stream = bind.sessionConnect(id: id, isFileTransfer: isFileTransfer);
// ignore: unused_local_variable
final addRes = bind.sessionAddSync(
id: id, isFileTransfer: isFileTransfer, isPortForward: isPortForward);
final stream = bind.sessionStart(id: id);
final cb = ffiModel.startEventListener(id);
() async {
await for (final message in stream) {
@ -1092,7 +1114,7 @@ class FFI {
ffiModel.clear();
canvasModel.clear();
resetModifiers();
print("model closed");
debugPrint("model $id closed");
}
/// Send **get** command to the Rust core based on [name] and [arg].
@ -1236,20 +1258,20 @@ class PeerInfo {
Future<void> savePreference(String id, double xCursor, double yCursor,
double xCanvas, double yCanvas, double scale, int currentDisplay) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final p = Map<String, dynamic>();
final p = <String, dynamic>{};
p['xCursor'] = xCursor;
p['yCursor'] = yCursor;
p['xCanvas'] = xCanvas;
p['yCanvas'] = yCanvas;
p['scale'] = scale;
p['currentDisplay'] = currentDisplay;
prefs.setString('peer' + id, json.encode(p));
prefs.setString('peer$id', json.encode(p));
}
Future<Map<String, dynamic>?> getPreference(String id) async {
if (!isWebDesktop) return null;
SharedPreferences prefs = await SharedPreferences.getInstance();
var p = prefs.getString('peer' + id);
var p = prefs.getString('peer$id');
if (p == null) return null;
Map<String, dynamic> m = json.decode(p);
return m;
@ -1257,7 +1279,7 @@ Future<Map<String, dynamic>?> getPreference(String id) async {
void removePreference(String id) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.remove('peer' + id);
prefs.remove('peer$id');
}
void initializeCursorAndCanvas(FFI ffi) async {

View File

@ -30,7 +30,7 @@ class PlatformFFI {
String _dir = '';
String _homeDir = '';
F2? _translate;
var _eventHandlers = Map<String, Map<String, HandleEvent>>();
final _eventHandlers = Map<String, Map<String, HandleEvent>>();
late RustdeskImpl _ffiBind;
late String _appType;
void Function(Map<String, dynamic>)? _eventCallback;
@ -50,27 +50,27 @@ class PlatformFFI {
}
bool registerEventHandler(
String event_name, String handler_name, HandleEvent handler) {
debugPrint('registerEventHandler $event_name $handler_name');
var handlers = _eventHandlers[event_name];
String eventName, String handlerName, HandleEvent handler) {
debugPrint('registerEventHandler $eventName $handlerName');
var handlers = _eventHandlers[eventName];
if (handlers == null) {
_eventHandlers[event_name] = {handler_name: handler};
_eventHandlers[eventName] = {handlerName: handler};
return true;
} else {
if (handlers.containsKey(handler_name)) {
if (handlers.containsKey(handlerName)) {
return false;
} else {
handlers[handler_name] = handler;
handlers[handlerName] = handler;
return true;
}
}
}
void unregisterEventHandler(String event_name, String handler_name) {
debugPrint('unregisterEventHandler $event_name $handler_name');
var handlers = _eventHandlers[event_name];
void unregisterEventHandler(String eventName, String handlerName) {
debugPrint('unregisterEventHandler $eventName $handlerName');
var handlers = _eventHandlers[eventName];
if (handlers != null) {
handlers.remove(handler_name);
handlers.remove(handlerName);
}
}
@ -117,7 +117,7 @@ class PlatformFFI {
_homeDir = (await getDownloadsDirectory())?.path ?? "";
}
} catch (e) {
print(e);
print("initialize failed: $e");
}
String id = 'NA';
String name = 'Flutter';
@ -151,7 +151,7 @@ class PlatformFFI {
await _ffiBind.mainSetHomeDir(home: _homeDir);
await _ffiBind.mainInit(appDir: _dir);
} catch (e) {
print(e);
print("initialize failed: $e");
}
version = await getVersion();
}

View File

@ -10,9 +10,8 @@ class Peer {
final List<dynamic> tags;
bool online = false;
Peer.fromJson(String id, Map<String, dynamic> json)
: id = id,
username = json['username'] ?? '',
Peer.fromJson(this.id, Map<String, dynamic> json)
: username = json['username'] ?? '',
hostname = json['hostname'] ?? '',
platform = json['platform'] ?? '',
tags = json['tags'] ?? [];
@ -35,57 +34,52 @@ class Peer {
}
class Peers extends ChangeNotifier {
late String _name;
late List<Peer> _peers;
late final _loadEvent;
final String name;
final String loadEvent;
List<Peer> peers;
static const _cbQueryOnlines = 'callback_query_onlines';
Peers(String name, String loadEvent, List<Peer> _initPeers) {
_name = name;
_loadEvent = loadEvent;
_peers = _initPeers;
platformFFI.registerEventHandler(_cbQueryOnlines, _name, (evt) {
Peers({required this.name, required this.peers, required this.loadEvent}) {
platformFFI.registerEventHandler(_cbQueryOnlines, name, (evt) {
_updateOnlineState(evt);
});
platformFFI.registerEventHandler(_loadEvent, _name, (evt) {
platformFFI.registerEventHandler(loadEvent, name, (evt) {
_updatePeers(evt);
});
}
List<Peer> get peers => _peers;
@override
void dispose() {
platformFFI.unregisterEventHandler(_cbQueryOnlines, _name);
platformFFI.unregisterEventHandler(_loadEvent, _name);
platformFFI.unregisterEventHandler(_cbQueryOnlines, name);
platformFFI.unregisterEventHandler(loadEvent, name);
super.dispose();
}
Peer getByIndex(int index) {
if (index < _peers.length) {
return _peers[index];
if (index < peers.length) {
return peers[index];
} else {
return Peer.loading();
}
}
int getPeersCount() {
return _peers.length;
return peers.length;
}
void _updateOnlineState(Map<String, dynamic> evt) {
evt['onlines'].split(',').forEach((online) {
for (var i = 0; i < _peers.length; i++) {
if (_peers[i].id == online) {
_peers[i].online = true;
for (var i = 0; i < peers.length; i++) {
if (peers[i].id == online) {
peers[i].online = true;
}
}
});
evt['offlines'].split(',').forEach((offline) {
for (var i = 0; i < _peers.length; i++) {
if (_peers[i].id == offline) {
_peers[i].online = false;
for (var i = 0; i < peers.length; i++) {
if (peers[i].id == offline) {
peers[i].online = false;
}
}
});
@ -95,19 +89,19 @@ class Peers extends ChangeNotifier {
void _updatePeers(Map<String, dynamic> evt) {
final onlineStates = _getOnlineStates();
_peers = _decodePeers(evt['peers']);
_peers.forEach((peer) {
peers = _decodePeers(evt['peers']);
for (var peer in peers) {
final state = onlineStates[peer.id];
peer.online = state != null && state != false;
});
}
notifyListeners();
}
Map<String, bool> _getOnlineStates() {
var onlineStates = new Map<String, bool>();
_peers.forEach((peer) {
var onlineStates = <String, bool>{};
for (var peer in peers) {
onlineStates[peer.id] = peer.online;
});
}
return onlineStates;
}
@ -121,7 +115,7 @@ class Peers extends ChangeNotifier {
Peer.fromJson(s[0] as String, s[1] as Map<String, dynamic>))
.toList();
} catch (e) {
print('peers(): $e');
debugPrint('peers(): $e');
}
return [];
}

View File

@ -1,5 +1,4 @@
import 'dart:convert';
import 'dart:ui';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
@ -35,10 +34,11 @@ class RustDeskMultiWindowManager {
int? _remoteDesktopWindowId;
int? _fileTransferWindowId;
int? _portForwardWindowId;
Future<dynamic> new_remote_desktop(String remote_id) async {
Future<dynamic> newRemoteDesktop(String remoteId) async {
final msg =
jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remote_id});
jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remoteId});
try {
final ids = await DesktopMultiWindow.getAllSubWindowIds();
@ -62,9 +62,9 @@ class RustDeskMultiWindowManager {
}
}
Future<dynamic> new_file_transfer(String remote_id) async {
Future<dynamic> newFileTransfer(String remoteId) async {
final msg =
jsonEncode({"type": WindowType.FileTransfer.index, "id": remote_id});
jsonEncode({"type": WindowType.FileTransfer.index, "id": remoteId});
try {
final ids = await DesktopMultiWindow.getAllSubWindowIds();
@ -87,6 +87,31 @@ class RustDeskMultiWindowManager {
}
}
Future<dynamic> newPortForward(String remoteId, bool isRDP) async {
final msg = jsonEncode(
{"type": WindowType.PortForward.index, "id": remoteId, "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 {
int? windowId = findWindowByType(type);
if (windowId == null) {
@ -104,7 +129,7 @@ class RustDeskMultiWindowManager {
case WindowType.FileTransfer:
return _fileTransferWindowId;
case WindowType.PortForward:
break;
return _portForwardWindowId;
case WindowType.Unknown:
break;
}

View File

@ -235,12 +235,10 @@ packages:
dash_chat_2:
dependency: "direct main"
description:
path: "."
ref: feat_maxWidth
resolved-ref: "3946ecf86d3600b54632fd80d0eb0ef0e74f2d6a"
url: "https://github.com/fufesou/Dash-Chat-2"
source: git
version: "0.0.12"
name: dash_chat_2
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.0.14"
desktop_drop:
dependency: "direct main"
description:
@ -252,8 +250,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: e013c81d75320bbf28adddeaadf462264ee6039d
resolved-ref: e013c81d75320bbf28adddeaadf462264ee6039d
ref: e0368a023ba195462acc00d33ab361b499f0e413
resolved-ref: e0368a023ba195462acc00d33ab361b499f0e413
url: "https://github.com/Kingtous/rustdesk_desktop_multi_window"
source: git
version: "0.1.0"
@ -849,7 +847,7 @@ packages:
source: hosted
version: "3.1.0"
rxdart:
dependency: transitive
dependency: "direct main"
description:
name: rxdart
url: "https://pub.flutter-io.cn"
@ -1244,8 +1242,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: a25f1776ccc1119cbb2a8541174293aa36d532ed
resolved-ref: a25f1776ccc1119cbb2a8541174293aa36d532ed
ref: "799ef079e87938c3f4340591b4330c2598f38bb9"
resolved-ref: "799ef079e87938c3f4340591b4330c2598f38bb9"
url: "https://github.com/Kingtous/rustdesk_window_manager"
source: git
version: "0.2.6"

View File

@ -34,16 +34,13 @@ dependencies:
provider: ^6.0.3
tuple: ^2.0.0
wakelock: ^0.5.2
device_info_plus: ^4.0.2
device_info_plus: ^4.1.2
firebase_analytics: ^9.1.5
package_info_plus: ^1.4.2
url_launcher: ^6.0.9
shared_preferences: ^2.0.6
toggle_switch: ^1.4.0
dash_chat_2:
git:
url: https://github.com/fufesou/Dash-Chat-2
ref: feat_maxWidth
dash_chat_2: ^0.0.14
draggable_float_widget: ^0.0.2
settings_ui: ^2.0.2
flutter_breadcrumb: ^1.0.1
@ -61,11 +58,11 @@ dependencies:
window_manager:
git:
url: https://github.com/Kingtous/rustdesk_window_manager
ref: a25f1776ccc1119cbb2a8541174293aa36d532ed
ref: 799ef079e87938c3f4340591b4330c2598f38bb9
desktop_multi_window:
git:
url: https://github.com/Kingtous/rustdesk_desktop_multi_window
ref: e013c81d75320bbf28adddeaadf462264ee6039d
ref: e0368a023ba195462acc00d33ab361b499f0e413
freezed_annotation: ^2.0.3
tray_manager:
git:
@ -76,6 +73,7 @@ dependencies:
contextmenu: ^3.0.0
desktop_drop: ^0.3.3
scroll_pos: ^0.3.0
rxdart: ^0.27.5
dev_dependencies:
flutter_launcher_icons: ^0.9.1

View File

@ -5,25 +5,36 @@ import os
import glob
from tabnanny import check
def pad_start(s, n, c = ' '):
if len(s) >= n:
return s
return c * (n - len(s)) + s
def safe_unicode(s):
res = ""
for c in s:
res += r"\u{}".format(pad_start(hex(ord(c))[2:], 4, '0'))
return res
def main():
print('export const LANGS = {')
for fn in glob.glob('../../../src/lang/*'):
lang = os.path.basename(fn)[:-3]
if lang == 'template': continue
print(' %s: {'%lang)
for ln in open(fn):
for ln in open(fn, encoding='utf-8'):
ln = ln.strip()
if ln.startswith('("'):
toks = ln.split('", "')
assert(len(toks) == 2)
a = toks[0][2:]
b = toks[1][:-3]
print(' "%s": "%s",'%(a, b))
print(' "%s": "%s",'%(safe_unicode(a), safe_unicode(b)))
print(' },')
print('}')
check_if_retry = ['', False]
KEY_MAP = ['', False]
for ln in open('../../../src/client.rs'):
for ln in open('../../../src/client.rs', encoding='utf-8'):
ln = ln.strip()
if 'check_if_retry' in ln:
check_if_retry[1] = True
@ -55,7 +66,7 @@ def main():
print('export const KEY_MAP: any = {')
print(KEY_MAP[0])
print('}')
for ln in open('../../../Cargo.toml'):
for ln in open('../../../Cargo.toml', encoding='utf-8'):
if ln.startswith('version ='):
print('export const ' + ln)

View File

@ -2,7 +2,7 @@ use std::{
collections::HashMap,
net::SocketAddr,
ops::{Deref, Not},
sync::{mpsc, Arc, Mutex, RwLock},
sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock},
};
pub use async_trait::async_trait;
@ -37,7 +37,6 @@ use hbb_common::{
};
pub use helper::LatencyController;
pub use helper::*;
use scrap::Image;
use scrap::{
codec::{Decoder, DecoderCfg},
VpxDecoderConfig, VpxVideoCodecId,
@ -47,7 +46,12 @@ pub use super::lang::*;
pub mod file_trait;
pub mod helper;
pub mod io_loop;
pub static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true);
pub static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true);
pub static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true);
pub const MILLI1: Duration = Duration::from_millis(1);
pub const SEC30: Duration = Duration::from_secs(30);
/// Client of the remote desktop.
@ -55,7 +59,23 @@ pub struct Client;
#[cfg(not(any(target_os = "android", target_os = "linux")))]
lazy_static::lazy_static! {
static ref AUDIO_HOST: Host = cpal::default_host();
static ref AUDIO_HOST: Host = cpal::default_host();
}
use rdev::{Event, EventType::*, Key as RdevKey, Keyboard as RdevKeyboard, KeyboardState};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
lazy_static::lazy_static! {
static ref ENIGO: Arc<Mutex<enigo::Enigo>> = Arc::new(Mutex::new(enigo::Enigo::new()));
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn get_key_state(key: enigo::Key) -> bool {
use enigo::KeyboardControllable;
#[cfg(target_os = "macos")]
if key == enigo::Key::NumLock {
return true;
}
ENIGO.lock().unwrap().get_key_state(key)
}
cfg_if::cfg_if! {
@ -846,8 +866,7 @@ impl VideoHandler {
#[derive(Default)]
pub struct LoginConfigHandler {
id: String,
pub is_file_transfer: bool,
is_port_forward: bool,
pub conn_type: ConnType,
hash: Hash,
password: Vec<u8>, // remember password for reconnect
pub remember: bool,
@ -886,12 +905,10 @@ impl LoginConfigHandler {
/// # Arguments
///
/// * `id` - id of peer
/// * `is_file_transfer` - Whether the connection is file transfer.
/// * `is_port_forward` - Whether the connection is port forward.
pub fn initialize(&mut self, id: String, is_file_transfer: bool, is_port_forward: bool) {
/// * `conn_type` - Connection type enum.
pub fn initialize(&mut self, id: String, conn_type: ConnType) {
self.id = id;
self.is_file_transfer = is_file_transfer;
self.is_port_forward = is_port_forward;
self.conn_type = conn_type;
let config = self.load_config();
self.remember = !config.password.is_empty();
self.config = config;
@ -1048,7 +1065,8 @@ impl LoginConfigHandler {
///
/// * `ignore_default` - If `true`, ignore the default value of the option.
fn get_option_message(&self, ignore_default: bool) -> Option<OptionMessage> {
if self.is_port_forward || self.is_file_transfer {
if self.conn_type.eq(&ConnType::FILE_TRANSFER) || self.conn_type.eq(&ConnType::PORT_FORWARD)
{
return None;
}
let mut n = 0;
@ -1094,7 +1112,8 @@ impl LoginConfigHandler {
}
pub fn get_option_message_after_login(&self) -> Option<OptionMessage> {
if self.is_port_forward || self.is_file_transfer {
if self.conn_type.eq(&ConnType::FILE_TRANSFER) || self.conn_type.eq(&ConnType::PORT_FORWARD)
{
return None;
}
let mut n = 0;
@ -1260,13 +1279,13 @@ impl LoginConfigHandler {
///
/// * `username` - The name of the peer.
/// * `pi` - The peer info.
pub fn handle_peer_info(&mut self, username: String, pi: PeerInfo) {
pub fn handle_peer_info(&mut self, pi: &PeerInfo) {
if !pi.version.is_empty() {
self.version = hbb_common::get_version_number(&pi.version);
}
self.features = pi.features.into_option();
self.features = pi.features.clone().into_option();
let serde = PeerInfoSerde {
username,
username: pi.username.clone(),
hostname: pi.hostname.clone(),
platform: pi.platform.clone(),
};
@ -1330,19 +1349,20 @@ impl LoginConfigHandler {
version: crate::VERSION.to_string(),
..Default::default()
};
if self.is_file_transfer {
lr.set_file_transfer(FileTransfer {
match self.conn_type {
ConnType::FILE_TRANSFER => lr.set_file_transfer(FileTransfer {
dir: self.get_remote_dir(),
show_hidden: !self.get_option("remote_show_hidden").is_empty(),
..Default::default()
});
} else if self.is_port_forward {
lr.set_port_forward(PortForward {
}),
ConnType::PORT_FORWARD => lr.set_port_forward(PortForward {
host: self.port_forward.0.clone(),
port: self.port_forward.1,
..Default::default()
});
}),
_ => {}
}
let mut msg_out = Message::new();
msg_out.set_login_request(lr);
msg_out
@ -1651,6 +1671,12 @@ pub trait Interface: Send + Clone + 'static + Sized {
fn handle_login_error(&mut self, err: &str) -> bool;
fn handle_peer_info(&mut self, pi: PeerInfo);
fn set_force_relay(&mut self, direct: bool, received: bool);
fn is_file_transfer(&self) -> bool;
fn is_port_forward(&self) -> bool;
fn is_rdp(&self) -> bool;
fn on_error(&self, err: &str) {
self.msgbox("error", "Error", err);
}
fn is_force_relay(&self) -> bool;
async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream);
async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream);

View File

@ -1,4 +1,4 @@
use hbb_common::{fs, message_proto::*};
use hbb_common::{fs, message_proto::*, log};
use super::{Data, Interface};
@ -114,4 +114,26 @@ pub trait FileManager: Interface {
fn resume_job(&self, id: i32, is_remote: bool) {
self.send(Data::ResumeJob((id, is_remote)));
}
fn set_confirm_override_file(
&self,
id: i32,
file_num: i32,
need_override: bool,
remember: bool,
is_upload: bool,
) {
log::info!(
"confirm file transfer, job: {}, need_override: {}",
id,
need_override
);
self.send(Data::SetConfirmOverrideFile((
id,
file_num,
need_override,
remember,
is_upload,
)));
}
}

1208
src/client/io_loop.rs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -13,21 +13,22 @@ use hbb_common::{
};
use hbb_common::{password_security, ResultType};
use crate::client::file_trait::FileManager;
use crate::{client::file_trait::FileManager, flutter::{session_add, session_start_}};
use crate::common::make_fd_to_json;
use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state};
use crate::flutter::{self, Session, SESSIONS};
use crate::flutter::{self, SESSIONS};
use crate::start_server;
use crate::ui_interface;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::ui_interface::{change_id, check_connect_status, is_ok_change_id};
use crate::ui_interface::{
check_super_user_permission, discover, forget_password, get_api_server, get_app_name,
get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_langs,
get_license, get_local_option, get_option, get_options, get_peer, get_peer_option, get_socks,
get_sound_inputs, get_uuid, get_version, has_hwcodec, has_rendezvous_service, post_request,
set_local_option, set_option, set_options, set_peer_option, set_permanent_password, set_socks,
store_fav, test_if_valid_server, update_temporary_password, using_public_server,
check_mouse_time, check_super_user_permission, discover, forget_password, get_api_server,
get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers,
get_langs, get_license, get_local_option, get_mouse_time, get_option, get_options, get_peer,
get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, has_hwcodec,
has_rendezvous_service, post_request, set_local_option, set_option, set_options,
set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server,
update_temporary_password, using_public_server,
};
fn initialize(app_dir: &str) {
@ -107,13 +108,18 @@ pub fn host_stop_system_key_propagate(stopped: bool) {
crate::platform::windows::stop_system_key_propagate(stopped);
}
pub fn session_connect(
events2ui: StreamSink<EventToUI>,
id: String,
is_file_transfer: bool,
) -> ResultType<()> {
Session::start(&id, is_file_transfer, events2ui);
Ok(())
// FIXME: -> ResultType<()> cannot be parsed by frb_codegen
// thread 'main' panicked at 'Failed to parse function output type `ResultType<()>`', $HOME\.cargo\git\checkouts\flutter_rust_bridge-ddba876d3ebb2a1e\e5adce5\frb_codegen\src\parser\mod.rs:151:25
pub fn session_add_sync(id: String, is_file_transfer: bool, is_port_forward: bool) -> SyncReturn<String> {
if let Err(e) = session_add(&id, is_file_transfer, is_port_forward) {
SyncReturn(format!("Failed to add session with id {}, {}", &id, e))
} else {
SyncReturn("".to_owned())
}
}
pub fn session_start(events2ui: StreamSink<EventToUI>, id: String) -> ResultType<()> {
session_start_(&id, events2ui)
}
pub fn session_get_remember(id: String) -> Option<bool> {
@ -126,7 +132,7 @@ pub fn session_get_remember(id: String) -> Option<bool> {
pub fn session_get_toggle_option(id: String, arg: String) -> Option<bool> {
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
Some(session.get_toggle_option(&arg))
Some(session.get_toggle_option(arg))
} else {
None
}
@ -137,17 +143,9 @@ pub fn session_get_toggle_option_sync(id: String, arg: String) -> SyncReturn<boo
SyncReturn(res)
}
pub fn session_get_image_quality(id: String) -> Option<String> {
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
Some(session.get_image_quality())
} else {
None
}
}
pub fn session_get_option(id: String, arg: String) -> Option<String> {
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
Some(session.get_option(&arg))
Some(session.get_option(arg))
} else {
None
}
@ -155,7 +153,7 @@ pub fn session_get_option(id: String, arg: String) -> Option<String> {
pub fn session_login(id: String, password: String, remember: bool) {
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
session.login(&password, remember);
session.login(password, remember);
}
}
@ -168,7 +166,7 @@ pub fn session_close(id: String) {
pub fn session_refresh(id: String) {
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
session.refresh();
session.refresh_video();
}
}
@ -179,14 +177,36 @@ pub fn session_reconnect(id: String) {
}
pub fn session_toggle_option(id: String, value: String) {
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
session.toggle_option(value);
}
}
pub fn session_get_image_quality(id: String) -> Option<String> {
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
session.toggle_option(&value);
Some(session.get_image_quality())
} else {
None
}
}
pub fn session_set_image_quality(id: String, value: String) {
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
session.save_image_quality(value);
}
}
pub fn session_get_custom_image_quality(id: String) -> Option<Vec<i32>> {
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
session.set_image_quality(&value);
Some(session.get_custom_image_quality())
} else {
None
}
}
pub fn session_set_custom_image_quality(id: String, value: i32) {
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
session.save_custom_image_quality(value);
}
}
@ -208,11 +228,11 @@ pub fn session_switch_display(id: String, value: i32) {
}
}
pub fn session_input_raw_key(id: String, keycode: i32, scancode:i32, down: bool){
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
session.input_raw_key(keycode, scancode, down);
}
}
// pub fn session_input_raw_key(id: String, keycode: i32, scancode:i32, down: bool){
// if let Some(session) = SESSIONS.read().unwrap().get(&id) {
// session.input_raw_key(keycode, scancode, down);
// }
// }
pub fn session_input_key(
id: String,
@ -250,7 +270,7 @@ pub fn session_peer_option(id: String, name: String, value: String) {
pub fn session_get_peer_option(id: String, name: String) -> String {
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
return session.get_option(&name);
return session.get_option(name);
}
"".to_string()
}
@ -349,7 +369,7 @@ pub fn session_get_platform(id: String, is_remote: bool) -> String {
pub fn session_load_last_transfer_jobs(id: String) {
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
return session.load_last_jobs();
// return session.load_last_jobs();
} else {
// a tip for flutter dev
eprintln!(
@ -473,7 +493,7 @@ pub fn main_get_connect_status() -> String {
pub fn main_check_connect_status() {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
check_connect_status(true);
check_mouse_time(); // avoid multi calls
}
pub fn main_is_using_public_server() -> bool {
@ -598,12 +618,32 @@ pub fn main_load_lan_peers() {
{
let data = HashMap::from([
("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()));
};
}
pub fn session_add_port_forward(
id: String,
local_port: i32,
remote_host: String,
remote_port: i32,
) {
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
session.add_port_forward(local_port, remote_host, remote_port);
}
}
pub fn session_remove_port_forward(id: String, local_port: i32) {
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
session.remove_port_forward(local_port);
}
}
pub fn main_get_last_remote_id() -> String {
// if !config::APP_DIR.read().unwrap().is_empty() {
// res = LocalConfig::get_remote_id();
@ -667,7 +707,6 @@ pub fn main_has_hwcodec() -> bool {
has_hwcodec()
}
// TODO
pub fn session_send_mouse(id: String, msg: String) {
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(&msg) {
let alt = m.get("alt").is_some();
@ -745,6 +784,14 @@ pub fn main_check_super_user_permission() -> bool {
check_super_user_permission()
}
pub fn main_check_mouse_time() {
check_mouse_time();
}
pub fn main_get_mouse_time() -> f64 {
get_mouse_time()
}
pub fn cm_send_chat(conn_id: i32, msg: String) {
connection_manager::send_chat(conn_id, msg);
}

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", "正在重启远程设备"),
("remote_restarting_tip", "远程设备正在重启, 请关闭当前提示框, 并在一段时间后使用永久密码重新连接"),
("Copied", "已复制"),
("Exit Fullscreen", "退出全屏"),
("Fullscreen", "全屏"),
("Mobile Actions", "移动端操作"),
("Select Monitor", "选择监视器"),
("Control Actions", "控制操作"),
("Display Settings", "显示设置"),
("Ratio", "比例"),
("Image Quality", "画质"),
("Scroll Style", "滚屏方式"),
("Show Menubar", "显示菜单栏"),
("Hide Menubar", "隐藏菜单栏"),
("Direct Connection", "直接连接"),
("Relay Connection", "中继连接"),
("Secure Connection", "安全连接"),
("Insecure Connection", "非安全连接"),
("Scale original", "原始尺寸"),
("Scale adaptive", "适应窗口"),
].iter().cloned().collect();
}

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", ""),
("remote_restarting_tip", ""),
("Copied", ""),
("Exit Fullscreen", "Ukončete celou obrazovku"),
("Fullscreen", "Celá obrazovka"),
("Mobile Actions", "Mobilní akce"),
("Select Monitor", "Vyberte možnost Monitor"),
("Control Actions", "Ovládací akce"),
("Display Settings", "Nastavení obrazovky"),
("Ratio", "Poměr"),
("Image Quality", "Kvalita obrazu"),
("Scroll Style", "Štýl posúvania"),
("Show Menubar", "Zobrazit panel nabídek"),
("Hide Menubar", "skrýt panel nabídek"),
("Direct Connection", "Přímé spojení"),
("Relay Connection", "Připojení relé"),
("Secure Connection", "Zabezpečené připojení"),
("Insecure Connection", "Nezabezpečené připojení"),
("Scale original", "Měřítko původní"),
("Scale adaptive", "Měřítko adaptivní"),
].iter().cloned().collect();
}

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", ""),
("remote_restarting_tip", ""),
("Copied", ""),
("Exit Fullscreen", "Afslut fuldskærm"),
("Fullscreen", "Fuld skærm"),
("Mobile Actions", "Mobile handlinger"),
("Select Monitor", "Vælg Monitor"),
("Control Actions", "Kontrolhandlinger"),
("Display Settings", "Skærmindstillinger"),
("Ratio", "Forhold"),
("Image Quality", "Billede kvalitet"),
("Scroll Style", "Rulstil"),
("Show Menubar", "Vis menulinje"),
("Hide Menubar", "skjul menulinjen"),
("Direct Connection", "Direkte forbindelse"),
("Relay Connection", "Relæforbindelse"),
("Secure Connection", "Sikker forbindelse"),
("Insecure Connection", "Usikker forbindelse"),
("Scale original", "Skala original"),
("Scale adaptive", "Skala adaptiv"),
].iter().cloned().collect();
}

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", "Entferntes Gerät wird neu gestartet"),
("remote_restarting_tip", "Entferntes Gerät startet neu, bitte schließen Sie diese Meldung und verbinden Sie sich mit dem dauerhaften Passwort erneut."),
("Copied", ""),
("Exit Fullscreen", "Vollbild beenden"),
("Fullscreen", "Ganzer Bildschirm"),
("Mobile Actions", "Mobile Aktionen"),
("Select Monitor", "Wählen Sie Überwachen aus"),
("Control Actions", "Kontrollaktionen"),
("Display Settings", "Bildschirmeinstellungen"),
("Ratio", "Verhältnis"),
("Image Quality", "Bildqualität"),
("Scroll Style", "Scroll-Stil"),
("Show Menubar", "Menüleiste anzeigen"),
("Hide Menubar", "Menüleiste ausblenden"),
("Direct Connection", "Direkte Verbindung"),
("Relay Connection", "Relaisverbindung"),
("Secure Connection", "Sichere Verbindung"),
("Insecure Connection", "Unsichere Verbindung"),
("Scale original", "Original skalieren"),
("Scale adaptive", "Adaptiv skalieren"),
].iter().cloned().collect();
}

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", ""),
("remote_restarting_tip", ""),
("Copied", ""),
("Exit Fullscreen", "Eliru Plenekranon"),
("Fullscreen", "Plenekrane"),
("Mobile Actions", "Poŝtelefonaj Agoj"),
("Select Monitor", "Elektu Monitoron"),
("Control Actions", "Kontrolaj Agoj"),
("Display Settings", "Montraj Agordoj"),
("Ratio", "Proporcio"),
("Image Quality", "Bilda Kvalito"),
("Scroll Style", "Ruluma Stilo"),
("Show Menubar", "Montru menubreton"),
("Hide Menubar", "kaŝi menubreton"),
("Direct Connection", "Rekta Konekto"),
("Relay Connection", "Relajsa Konekto"),
("Secure Connection", "Sekura Konekto"),
("Insecure Connection", "Nesekura Konekto"),
("Scale original", "Skalo originalo"),
("Scale adaptive", "Skalo adapta"),
].iter().cloned().collect();
}

View File

@ -318,5 +318,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", "Reiniciando dispositivo remoto"),
("remote_restarting_tip", "Dispositivo remoto reiniciando, favor de cerrar este mensaje y reconectarse con la contraseña permamente despues de un momento."),
("Copied", ""),
("Exit Fullscreen", "Salir de pantalla completa"),
("Fullscreen", "Pantalla completa"),
("Mobile Actions", "Acciones móviles"),
("Select Monitor", "Seleccionar monitor"),
("Control Actions", "Acciones de control"),
("Display Settings", "Configuración de pantalla"),
("Ratio", "Relación"),
("Image Quality", "La calidad de imagen"),
("Scroll Style", "Estilo de desplazamiento"),
("Show Menubar", "ajustes de pantalla"),
("Hide Menubar", "ocultar barra de menú"),
("Direct Connection", "Conexión directa"),
("Relay Connection", "Conexión de relé"),
("Secure Connection", "Conexión segura"),
("Insecure Connection", "Conexión insegura"),
("Scale original", "escala originales"),
("Scale adaptive", "Adaptable a escala"),
].iter().cloned().collect();
}

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", ""),
("remote_restarting_tip", ""),
("Copied", ""),
("Exit Fullscreen", "Quitter le mode plein écran"),
("Fullscreen", "Plein écran"),
("Mobile Actions", "Actions mobiles"),
("Select Monitor", "Sélectionnez Moniteur"),
("Control Actions", "Actions de contrôle"),
("Display Settings", "Paramètres d'affichage"),
("Ratio", "Rapport"),
("Image Quality", "Qualité d'image"),
("Scroll Style", "Style de défilement"),
("Show Menubar", "Afficher la barre de menus"),
("Hide Menubar", "masquer la barre de menus"),
("Direct Connection", "Connexion directe"),
("Relay Connection", "Connexion relais"),
("Secure Connection", "Connexion sécurisée"),
("Insecure Connection", "Connexion non sécurisée"),
("Scale original", "Échelle d'origine"),
("Scale adaptive", "Échelle adaptative"),
].iter().cloned().collect();
}

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", ""),
("remote_restarting_tip", ""),
("Copied", ""),
("Exit Fullscreen", "Lépjen ki a teljes képernyőről"),
("Fullscreen", "Teljes képernyő"),
("Mobile Actions", "mobil műveletek"),
("Select Monitor", "Válassza a Monitor lehetőséget"),
("Control Actions", "Irányítási műveletek"),
("Display Settings", "Megjelenítési beállítások"),
("Ratio", "Hányados"),
("Image Quality", "Képminőség"),
("Scroll Style", "Görgetési stílus"),
("Show Menubar", "Menüsor megjelenítése"),
("Hide Menubar", "menüsor elrejtése"),
("Direct Connection", "Közvetlen kapcsolat"),
("Relay Connection", "Relé csatlakozás"),
("Secure Connection", "Biztonságos kapcsolat"),
("Insecure Connection", "Nem biztonságos kapcsolat"),
("Scale original", "Eredeti méretarány"),
("Scale adaptive", "Skála adaptív"),
].iter().cloned().collect();
}

View File

@ -318,5 +318,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", "Memulai Ulang Perangkat Jarak Jauh"),
("remote_restarting_tip", ""),
("Copied", ""),
("Exit Fullscreen", "Keluar dari Layar Penuh"),
("Fullscreen", "Layar penuh"),
("Mobile Actions", "Tindakan Seluler"),
("Select Monitor", "Pilih Monitor"),
("Control Actions", "Tindakan Kontrol"),
("Display Settings", "Pengaturan tampilan"),
("Ratio", "Perbandingan"),
("Image Quality", "Kualitas gambar"),
("Scroll Style", "Gaya Gulir"),
("Show Menubar", "Tampilkan bilah menu"),
("Hide Menubar", "sembunyikan bilah menu"),
("Direct Connection", "Koneksi langsung"),
("Relay Connection", "Koneksi Relay"),
("Secure Connection", "Koneksi aman"),
("Insecure Connection", "Koneksi Tidak Aman"),
("Scale original", "Skala asli"),
("Scale adaptive", "Skala adaptif"),
].iter().cloned().collect();
}

View File

@ -301,6 +301,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Are you sure you want to restart", "Sei sicuro di voler riavviare?"),
("Restarting Remote Device", "Il dispositivo remoto si sta riavviando"),
("remote_restarting_tip", "Riavviare il dispositivo remoto"),
("Exit Fullscreen", "Esci dalla modalità schermo intero"),
("Fullscreen", "A schermo intero"),
("Mobile Actions", "Azioni mobili"),
("Select Monitor", "Seleziona Monitora"),
("Control Actions", "Azioni di controllo"),
("Display Settings", "Impostazioni di visualizzazione"),
("Ratio", "Rapporto"),
("Image Quality", "Qualità dell'immagine"),
("Scroll Style", "Stile di scorrimento"),
("Show Menubar", "Mostra la barra dei menu"),
("Hide Menubar", "nascondi la barra dei menu"),
("Direct Connection", "Connessione diretta"),
("Relay Connection", "Collegamento a relè"),
("Secure Connection", "Connessione sicura"),
("Insecure Connection", "Connessione insicura"),
("Scale original", "Scala originale"),
("Scale adaptive", "Scala adattiva"),
("Legacy mode", ""),
("Map mode", ""),
("Translate mode", ""),

View File

@ -302,5 +302,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Are you sure you want to restart", "本当に再起動しますか"),
("Restarting Remote Device", "リモート端末を再起動中"),
("remote_restarting_tip", "リモート端末は再起動中です。このメッセージボックスを閉じて、しばらくした後に固定のパスワードを使用して再接続してください。"),
("Exit Fullscreen", "全画面表示を終了"),
("Fullscreen", "全画面表示"),
("Mobile Actions", "モバイル アクション"),
("Select Monitor", "モニターを選択"),
("Control Actions", "コントロール アクション"),
("Display Settings", "ディスプレイの設定"),
("Ratio", "比率"),
("Image Quality", "画質"),
("Scroll Style", "スクロール スタイル"),
("Show Menubar", "メニューバーを表示"),
("Hide Menubar", "メニューバーを隠す"),
("Direct Connection", "直接接続"),
("Relay Connection", "リレー接続"),
("Secure Connection", "安全な接続"),
("Insecure Connection", "安全でない接続"),
("Scale original", "オリジナルサイズ"),
("Scale adaptive", "フィットウィンドウ"),
].iter().cloned().collect();
}

View File

@ -299,5 +299,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Are you sure you want to restart", "정말로 재시작 하시겠습니까"),
("Restarting Remote Device", "원격 기기를 다시 시작하는중"),
("remote_restarting_tip", "원격 장치를 다시 시작하는 중입니다. 이 메시지 상자를 닫고 잠시 후 영구 비밀번호로 다시 연결하십시오."),
("Exit Fullscreen", "전체 화면 종료"),
("Fullscreen", "전체화면"),
("Mobile Actions", "모바일 액션"),
("Select Monitor", "모니터 선택"),
("Control Actions", "제어 작업"),
("Display Settings", "화면 설정"),
("Ratio", "비율"),
("Image Quality", "이미지 품질"),
("Scroll Style", "스크롤 스타일"),
("Show Menubar", "메뉴 표시줄 표시"),
("Hide Menubar", "메뉴 표시줄 숨기기"),
("Direct Connection", "직접 연결"),
("Relay Connection", "릴레이 연결"),
("Secure Connection", "보안 연결"),
("Insecure Connection", "안전하지 않은 연결"),
("Scale original", "원래 크기"),
("Scale adaptive", "맞는 창"),
].iter().cloned().collect();
}

View File

@ -303,5 +303,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Set security password", "Ustaw hasło zabezpieczające"),
("Connection not allowed", "Połączenie niedozwolone"),
("Copied", ""),
("Exit Fullscreen", "Wyłączyć tryb pełnoekranowy"),
("Fullscreen", "Pełny ekran"),
("Mobile Actions", "Działania mobilne"),
("Select Monitor", "Wybierz Monitor"),
("Control Actions", "Działania kontrolne"),
("Display Settings", "Ustawienia wyświetlania"),
("Ratio", "Stosunek"),
("Image Quality", "Jakość obrazu"),
("Scroll Style", "Styl przewijania"),
("Show Menubar", "Pokaż pasek menu"),
("Hide Menubar", "ukryj pasek menu"),
("Direct Connection", "Bezpośrednie połączenie"),
("Relay Connection", "Połączenie przekaźnika"),
("Secure Connection", "Bezpieczne połączenie"),
("Insecure Connection", "Niepewne połączenie"),
("Scale original", "Skala oryginalna"),
("Scale adaptive", "Skala adaptacyjna"),
].iter().cloned().collect();
}

View File

@ -299,5 +299,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Are you sure you want to restart", "Tem a certeza que pretende reiniciar"),
("Restarting Remote Device", "A reiniciar sistema remoto"),
("remote_restarting_tip", ""),
("Exit Fullscreen", "Sair da tela cheia"),
("Fullscreen", "Tela cheia"),
("Mobile Actions", "Ações para celular"),
("Select Monitor", "Selecionar monitor"),
("Control Actions", "Ações de controle"),
("Display Settings", "Configurações do visor"),
("Ratio", "Razão"),
("Image Quality", "Qualidade da imagem"),
("Scroll Style", "Estilo de rolagem"),
("Show Menubar", "Mostrar barra de menus"),
("Hide Menubar", "ocultar barra de menu"),
("Direct Connection", "Conexão direta"),
("Relay Connection", "Conexão de relé"),
("Secure Connection", "Conexão segura"),
("Insecure Connection", "Conexão insegura"),
("Scale original", "Escala original"),
("Scale adaptive", "Escala adaptável"),
].iter().cloned().collect();
}

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", ""),
("remote_restarting_tip", ""),
("Copied", ""),
("Exit Fullscreen", ""),
("Fullscreen", ""),
("Mobile Actions", ""),
("Select Monitor", ""),
("Control Actions", ""),
("Display Settings", ""),
("Ratio", ""),
("Image Quality", ""),
("Scroll Style", ""),
("Show Menubar", ""),
("Hide Menubar", ""),
("Direct Connection", ""),
("Relay Connection", ""),
("Secure Connection", ""),
("Insecure Connection", ""),
("Scale original", ""),
("Scale adaptive", ""),
].iter().cloned().collect();
}

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", "Перезагрузка удаленного устройства"),
("remote_restarting_tip", "Удаленное устройство перезапускается. Пожалуйста, закройте это сообщение и через некоторое время переподключитесь, используя постоянный пароль."),
("Copied", ""),
("Exit Fullscreen", "Выйти из полноэкранного режима"),
("Fullscreen", "Полноэкранный"),
("Mobile Actions", "Мобильные действия"),
("Select Monitor", "Выберите монитор"),
("Control Actions", "Действия по управлению"),
("Display Settings", "Настройки отображения"),
("Ratio", "Соотношение"),
("Image Quality", "Качество изображения"),
("Scroll Style", "Стиль прокрутки"),
("Show Menubar", "Показать строку меню"),
("Hide Menubar", "скрыть строку меню"),
("Direct Connection", "Прямая связь"),
("Relay Connection", "Релейное соединение"),
("Secure Connection", "Безопасное соединение"),
("Insecure Connection", "Небезопасное соединение"),
("Scale original", "Оригинал масштаба"),
("Scale adaptive", "Масштаб адаптивный"),
].iter().cloned().collect();
}

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", ""),
("remote_restarting_tip", ""),
("Copied", ""),
("Exit Fullscreen", "Ukončiť celú obrazovku"),
("Fullscreen", "Celá obrazovka"),
("Mobile Actions", "Mobilné akcie"),
("Select Monitor", "Vyberte možnosť Monitor"),
("Control Actions", "Kontrolné akcie"),
("Display Settings", "Nastavenia displeja"),
("Ratio", "Pomer"),
("Image Quality", "Kvalita obrazu"),
("Scroll Style", "Štýl posúvania"),
("Show Menubar", "Zobraziť panel s ponukami"),
("Hide Menubar", "skryť panel s ponukami"),
("Direct Connection", "Priame pripojenie"),
("Relay Connection", "Reléové pripojenie"),
("Secure Connection", "Zabezpečené pripojenie"),
("Insecure Connection", "Nezabezpečené pripojenie"),
("Scale original", "Pôvodná mierka"),
("Scale adaptive", "Prispôsobivá mierka"),
].iter().cloned().collect();
}

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", ""),
("remote_restarting_tip", ""),
("Copied", ""),
("Exit Fullscreen", ""),
("Fullscreen", ""),
("Mobile Actions", ""),
("Select Monitor", ""),
("Control Actions", ""),
("Display Settings", ""),
("Ratio", ""),
("Image Quality", ""),
("Scroll Style", ""),
("Show Menubar", ""),
("Hide Menubar", ""),
("Direct Connection", ""),
("Relay Connection", ""),
("Secure Connection", ""),
("Insecure Connection", ""),
("Scale original", ""),
("Scale adaptive", ""),
].iter().cloned().collect();
}

View File

@ -318,5 +318,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", "Uzaktan yeniden başlatılıyor"),
("remote_restarting_tip", ""),
("Copied", ""),
("Exit Fullscreen", "Tam ekrandan çık"),
("Fullscreen", "Tam ekran"),
("Mobile Actions", "Mobil İşlemler"),
("Select Monitor", "Monitörü Seç"),
("Control Actions", "Kontrol Eylemleri"),
("Display Settings", "Görüntü ayarları"),
("Ratio", "Oran"),
("Image Quality", "Görüntü kalitesi"),
("Scroll Style", "Kaydırma Stili"),
("Show Menubar", "Menü çubuğunu göster"),
("Hide Menubar", "menü çubuğunu gizle"),
("Direct Connection", "Doğrudan Bağlantı"),
("Relay Connection", "Röle Bağlantısı"),
("Secure Connection", "Güvenli bağlantı"),
("Insecure Connection", "Güvenli Bağlantı"),
("Scale original", "Orijinali ölçeklendir"),
("Scale adaptive", "Ölçek uyarlanabilir"),
].iter().cloned().collect();
}

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", "正在重啓遠程設備"),
("remote_restarting_tip", "遠程設備正在重啓,請關閉當前提示框,並在一段時間後使用永久密碼重新連接"),
("Copied", "已複製"),
("Exit Fullscreen", "退出全屏"),
("Fullscreen", "全屏"),
("Mobile Actions", "移動端操作"),
("Select Monitor", "選擇監視器"),
("Control Actions", "控制操作"),
("Display Settings", "顯示設置"),
("Ratio", "比例"),
("Image Quality", "畫質"),
("Scroll Style", "滾動樣式"),
("Show Menubar", "顯示菜單欄"),
("Hide Menubar", "隱藏菜單欄"),
("Direct Connection", "直接連接"),
("Relay Connection", "中繼連接"),
("Secure Connection", "安全連接"),
("Insecure Connection", "非安全連接"),
("Scale original", "原始尺寸"),
("Scale adaptive", "適應窗口"),
].iter().cloned().collect();
}

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", "Đang khởi động lại thiết bị từ xa"),
("remote_restarting_tip", "Thiết bị từ xa đang khởi động lại, hãy đóng cửa sổ tin nhắn này và kết nối lại với mật khẩu vĩnh viễn sau một khoảng thời gian"),
("Copied", ""),
("Exit Fullscreen", "Thoát toàn màn hình"),
("Fullscreen", "Toàn màn hình"),
("Mobile Actions", "Hành động trên thiết bị di động"),
("Select Monitor", "Chọn màn hình"),
("Control Actions", "Kiểm soát hành động"),
("Display Settings", "Thiết lập hiển thị"),
("Ratio", "Tỉ lệ"),
("Image Quality", "Chất lượng hình ảnh"),
("Scroll Style", "Kiểu cuộn"),
("Show Menubar", "Hiển thị thanh menu"),
("Hide Menubar", "ẩn thanh menu"),
("Direct Connection", "Kết nối trực tiếp"),
("Relay Connection", "Kết nối chuyển tiếp"),
("Secure Connection", "Kết nối an toàn"),
("Insecure Connection", "Kết nối không an toàn"),
("Scale original", "Quy mô gốc"),
("Scale adaptive", "Quy mô thích ứng"),
].iter().cloned().collect();
}

View File

@ -48,6 +48,7 @@ mod port_forward;
mod tray;
mod ui_interface;
mod ui_session_interface;
#[cfg(windows)]
pub mod clipboard_file;

View File

@ -1,3 +1,5 @@
use std::sync::{Arc, RwLock};
use crate::client::*;
use hbb_common::{
allow_err, bail,
@ -48,6 +50,9 @@ pub async fn listen(
ui_receiver: mpsc::UnboundedReceiver<Data>,
key: &str,
token: &str,
lc: Arc<RwLock<LoginConfigHandler>>,
remote_host: String,
remote_port: i32,
) -> ResultType<()> {
let listener = tcp::new_listener(format!("0.0.0.0:{}", port), true).await?;
let addr = listener.local_addr()?;
@ -61,6 +66,7 @@ pub async fn listen(
tokio::select! {
Ok((forward, addr)) = listener.accept() => {
log::info!("new connection from {:?}", addr);
lc.write().unwrap().port_forward = (remote_host.clone(), remote_port);
let id = id.clone();
let password = password.clone();
let mut forward = Framed::new(forward, BytesCodec::new());

View File

@ -950,6 +950,7 @@ impl Connection {
addr
))
.await;
return false;
}
}
}

View File

@ -146,7 +146,7 @@ pub fn start(args: &mut [String]) {
let args: Vec<String> = iter.map(|x| x.clone()).collect();
frame.set_title(&id);
frame.register_behavior("native-remote", move || {
Box::new(remote::Handler::new(
Box::new(remote::SciterSession::new(
cmd.clone(),
id.clone(),
pass.clone(),

File diff suppressed because it is too large Load Diff

1305
src/ui_session_interface.rs Normal file

File diff suppressed because it is too large Load Diff