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. 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 | | Location | Vendor | Specification |
| --------- | ------------- | ------------------ | | --------- | ------------- | ------------------ |
| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
| Singapore | Vultr | 1 VCPU / 1GB RAM | | Singapore | Vultr | 1 vCPU / 1GB RAM |
| Germany | Hetzner | 2 VCPU / 4GB RAM | | Germany | Hetzner | 2 vCPU / 4GB RAM |
| Germany | Codext | 4 VCPU / 8GB RAM | | Germany | Codext | 4 vCPU / 8GB RAM |
## Dependencies ## Dependencies

2
flutter/.gitignore vendored
View File

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

View File

@ -427,7 +427,45 @@ class CustomAlertDialog extends StatelessWidget {
void msgBox( void msgBox(
String type, String title, String text, OverlayDialogManager dialogManager, String type, String title, String text, OverlayDialogManager dialogManager,
{bool? hasCancel}) { {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), padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
//limits the touch area to the button area //limits the touch area to the button area
@ -439,41 +477,16 @@ void msgBox(
onPressed: onPressed, onPressed: onPressed,
child: child:
Text(translate(text), style: TextStyle(color: MyTheme.accent)))); 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(); 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( dialogManager.show((setState, close) => CustomAlertDialog(
title: Text(translate(title), style: TextStyle(fontSize: 21)), title: _msgBoxTitle(title),
content: Text(translate(text), style: TextStyle(fontSize: 15)), content: content,
actions: buttons)); actions: buttons));
} }
@ -492,13 +505,13 @@ const G = M * K;
String readableFileSize(double size) { String readableFileSize(double size) {
if (size < K) { if (size < K) {
return size.toStringAsFixed(2) + " B"; return "${size.toStringAsFixed(2)} B";
} else if (size < M) { } else if (size < M) {
return (size / K).toStringAsFixed(2) + " KB"; return "${(size / K).toStringAsFixed(2)} KB";
} else if (size < G) { } else if (size < G) {
return (size / M).toStringAsFixed(2) + " MB"; return "${(size / M).toStringAsFixed(2)} MB";
} else { } 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"); debugPrint("_globalFFI init end");
// after `put`, can also be globally found by Get.find<FFI>(); // after `put`, can also be globally found by Get.find<FFI>();
Get.put(_globalFFI, permanent: true); Get.put(_globalFFI, permanent: true);
// trigger connection status updater
await bind.mainCheckConnectStatus();
// global shared preference // global shared preference
await Get.putAsync(() => SharedPreferences.getInstance()); 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 kAppTypeMain = "main";
const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopRemote = "remote";
const String kAppTypeDesktopFileTransfer = "file transfer"; const String kAppTypeDesktopFileTransfer = "file transfer";
const String kAppTypeDesktopPortForward = "port forward";
const String kTabLabelHomePage = "Home"; const String kTabLabelHomePage = "Home";
const String kTabLabelSettingPage = "Settings"; const String kTabLabelSettingPage = "Settings";
const int kDefaultDisplayWidth = 1280; const int kMobileDefaultDisplayWidth = 720;
const int kDefaultDisplayHeight = 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(); final _idController = TextEditingController();
/// Update url. If it's not null, means an update is available. /// Update url. If it's not null, means an update is available.
var _updateUrl = ''; final _updateUrl = '';
Timer? _updateTimer; Timer? _updateTimer;
@ -92,7 +92,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
if (snapshot.hasData) { if (snapshot.hasData) {
return snapshot.data!; return snapshot.data!;
} else { } else {
return Offstage(); return const Offstage();
} }
}), }),
], ],
@ -110,7 +110,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
/// Callback for the connect button. /// Callback for the connect button.
/// Connects to the selected peer. /// Connects to the selected peer.
void onConnect({bool isFileTransfer = false}) { void onConnect({bool isFileTransfer = false}) {
var id = _idController.text.trim(); final id = _idController.text.trim();
connect(id, isFileTransfer: isFileTransfer); connect(id, isFileTransfer: isFileTransfer);
} }
@ -120,9 +120,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
if (id == '') return; if (id == '') return;
id = id.replaceAll(' ', ''); id = id.replaceAll(' ', '');
if (isFileTransfer) { if (isFileTransfer) {
await rustDeskWinManager.new_file_transfer(id); await rustDeskWinManager.newFileTransfer(id);
} else { } else {
await rustDeskWinManager.new_remote_desktop(id); await rustDeskWinManager.newRemoteDesktop(id);
} }
FocusScopeNode currentFocus = FocusScope.of(context); FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) { if (!currentFocus.hasPrimaryFocus) {
@ -233,7 +233,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
}, },
child: Container( child: Container(
height: 24, height: 24,
width: 72,
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: ftPressed.value color: ftPressed.value
@ -257,7 +256,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
color: ftPressed.value color: ftPressed.value
? MyTheme.color(context).bg ? MyTheme.color(context).bg
: MyTheme.color(context).text), : MyTheme.color(context).text),
), ).marginSymmetric(horizontal: 12),
), ),
)), )),
SizedBox( SizedBox(
@ -272,7 +271,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
onTap: onConnect, onTap: onConnect,
child: Container( child: Container(
height: 24, height: 24,
width: 65,
decoration: BoxDecoration( decoration: BoxDecoration(
color: connPressed.value color: connPressed.value
? MyTheme.accent ? MyTheme.accent
@ -289,12 +287,12 @@ class _ConnectionPageState extends State<ConnectionPage> {
child: Center( child: Center(
child: Text( child: Text(
translate( translate(
"Connection", "Connect",
), ),
style: TextStyle( style: TextStyle(
fontSize: 12, color: MyTheme.color(context).bg), 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:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.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/consts.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../models/model.dart';
class ConnectionTabPage extends StatefulWidget { class ConnectionTabPage extends StatefulWidget {
final Map<String, dynamic> params; final Map<String, dynamic> params;
@ -22,26 +21,27 @@ class ConnectionTabPage extends StatefulWidget {
class _ConnectionTabPageState extends State<ConnectionTabPage> { class _ConnectionTabPageState extends State<ConnectionTabPage> {
final tabController = Get.put(DesktopTabController()); final tabController = Get.put(DesktopTabController());
static final Rx<String> _fullscreenID = "".obs; static const IconData selectedIcon = Icons.desktop_windows_sharp;
static final IconData selectedIcon = Icons.desktop_windows_sharp; static const IconData unselectedIcon = Icons.desktop_windows_outlined;
static final IconData unselectedIcon = Icons.desktop_windows_outlined;
var connectionMap = RxList<Widget>.empty(growable: true); var connectionMap = RxList<Widget>.empty(growable: true);
_ConnectionTabPageState(Map<String, dynamic> params) { _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( tabController.add(TabInfo(
key: params['id'], key: peerId,
label: params['id'], label: peerId,
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon, unselectedIcon: unselectedIcon,
page: RemotePage( page: Obx(() => RemotePage(
key: ValueKey(params['id']), key: ValueKey(peerId),
id: params['id'], id: peerId,
tabBarHeight: tabBarHeight:
_fullscreenID.value.isNotEmpty ? 0 : kDesktopRemoteTabBarHeight, fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight,
fullscreenID: _fullscreenID, ))));
)));
} }
} }
@ -54,33 +54,27 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
rustDeskWinManager.setMethodHandler((call, fromWindowId) async { rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
print( print(
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); "call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
final RxBool fullscreen = Get.find(tag: 'fullscreen');
// for simplify, just replace connectionId // for simplify, just replace connectionId
if (call.method == "new_remote_desktop") { if (call.method == "new_remote_desktop") {
final args = jsonDecode(call.arguments); final args = jsonDecode(call.arguments);
final id = args['id']; final id = args['id'];
window_on_top(windowId()); window_on_top(windowId());
ConnectionTypeState.init(id);
tabController.add(TabInfo( tabController.add(TabInfo(
key: id, key: id,
label: id, label: id,
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon, unselectedIcon: unselectedIcon,
page: RemotePage( page: Obx(() => RemotePage(
key: ValueKey(id), key: ValueKey(id),
id: id, id: id,
tabBarHeight: _fullscreenID.value.isNotEmpty tabBarHeight:
? 0 fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight,
: kDesktopRemoteTabBarHeight, ))));
fullscreenID: _fullscreenID,
)));
} else if (call.method == "onDestroy") { } else if (call.method == "onDestroy") {
tabController.state.value.tabs.forEach((tab) { tabController.clear();
print("executing onDestroy hook, closing ${tab.label}}");
final tag = tab.label;
ffi(tag).close().then((_) {
Get.delete<FFI>(tag: tag);
});
});
Get.back();
} }
}); });
} }
@ -88,36 +82,79 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(); final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
return SubWindowDragToResizeArea( final RxBool fullscreen = Get.find(tag: 'fullscreen');
windowId: windowId(), return Obx(() => SubWindowDragToResizeArea(
child: Container( resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
decoration: BoxDecoration( windowId: windowId(),
border: Border.all(color: MyTheme.color(context).border!)), child: Container(
child: Scaffold( decoration: BoxDecoration(
backgroundColor: MyTheme.color(context).bg, border: Border.all(color: MyTheme.color(context).border!)),
body: Obx(() => DesktopTab( child: Scaffold(
controller: tabController, backgroundColor: MyTheme.color(context).bg,
theme: theme, body: Obx(() => DesktopTab(
isMainWindow: false, controller: tabController,
showTabBar: _fullscreenID.value.isEmpty, theme: theme,
tail: AddButton( tabType: DesktopTabType.remoteScreen,
theme: theme, showTabBar: fullscreen.isFalse,
).paddingOnly(left: 10), onClose: () {
pageViewBuilder: (pageView) { tabController.clear();
WindowController.fromWindowId(windowId()) },
.setFullscreen(_fullscreenID.value.isNotEmpty); tail: AddButton(
return pageView; theme: theme,
}, ).paddingOnly(left: 10),
))), pageViewBuilder: (pageView) {
), WindowController.fromWindowId(windowId())
); .setFullscreen(fullscreen.isTrue);
return pageView;
},
tabBuilder: (key, icon, label, themeConf) => Obx(() {
final connectionType = ConnectionTypeState.find(key);
if (!connectionType.isValid()) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
label,
],
);
} else {
final msgDirect = translate(
connectionType.direct.value ==
ConnectionType.strDirect
? 'Direct Connection'
: 'Relay Connection');
final msgSecure = translate(
connectionType.secure.value ==
ConnectionType.strSecure
? 'Secure Connection'
: 'Insecure Connection');
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
Tooltip(
message: '$msgDirect\n$msgSecure',
child: Image.asset(
'assets/${connectionType.secure.value}${connectionType.direct.value}.png',
width: themeConf.iconSize,
height: themeConf.iconSize,
).paddingOnly(right: 5),
),
label,
],
);
}
}),
))),
),
));
} }
void onRemoveId(String id) { void onRemoveId(String id) {
ffi(id).close(); if (tabController.state.value.tabs.isEmpty) {
if (tabController.state.value.tabs.length == 0) { WindowController.fromWindowId(windowId()).hide();
WindowController.fromWindowId(windowId()).close();
} }
ConnectionTypeState.delete(id);
} }
int windowId() { int windowId() {

View File

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

View File

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

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

View File

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

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

View File

@ -111,7 +111,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
showMaximize: false, showMaximize: false,
showMinimize: false, showMinimize: false,
controller: serverModel.tabController, controller: serverModel.tabController,
isMainWindow: true, tabType: DesktopTabType.cm,
pageViewBuilder: (pageView) => Row(children: [ pageViewBuilder: (pageView) => Row(children: [
Expanded(child: pageView), Expanded(child: pageView),
Consumer<ChatModel>( Consumer<ChatModel>(
@ -294,7 +294,8 @@ class _CmHeaderState extends State<_CmHeader>
Offstage( Offstage(
offstage: client.isFileTransfer, offstage: client.isFileTransfer,
child: IconButton( child: IconButton(
onPressed: () => gFFI.chatModel.toggleCMChatPage(client.id), onPressed: () => checkClickTime(
client.id, () => gFFI.chatModel.toggleCMChatPage(client.id)),
icon: Icon(Icons.message_outlined), icon: Icon(Icons.message_outlined),
), ),
) )
@ -326,7 +327,8 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey), BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey),
padding: EdgeInsets.all(4.0), padding: EdgeInsets.all(4.0),
child: InkWell( child: InkWell(
onTap: () => onTap?.call(!enabled), onTap: () =>
checkClickTime(widget.client.id, () => onTap?.call(!enabled)),
child: Image( child: Image(
image: icon, image: icon,
width: 50, width: 50,
@ -422,7 +424,8 @@ class _CmControlPanel extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.redAccent, borderRadius: BorderRadius.circular(10)), color: Colors.redAccent, borderRadius: BorderRadius.circular(10)),
child: InkWell( child: InkWell(
onTap: () => handleDisconnect(context), onTap: () =>
checkClickTime(client.id, () => handleDisconnect(context)),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@ -447,7 +450,8 @@ class _CmControlPanel extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: MyTheme.accent, borderRadius: BorderRadius.circular(10)), color: MyTheme.accent, borderRadius: BorderRadius.circular(10)),
child: InkWell( child: InkWell(
onTap: () => handleAccept(context), onTap: () =>
checkClickTime(client.id, () => handleAccept(context)),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@ -469,7 +473,8 @@ class _CmControlPanel extends StatelessWidget {
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey)), border: Border.all(color: Colors.grey)),
child: InkWell( child: InkWell(
onTap: () => handleDisconnect(context), onTap: () =>
checkClickTime(client.id, () => handleDisconnect(context)),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/connection_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/connection_tab_page.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
/// multi-tab desktop remote screen /// multi-tab desktop remote screen
@ -11,6 +12,8 @@ class DesktopRemoteScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
RxBool fullscreen = false.obs;
Get.put(fullscreen, tag: 'fullscreen');
return MultiProvider( return MultiProvider(
providers: [ providers: [
ChangeNotifierProvider.value(value: gFFI.ffiModel), 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); TextEditingController(text: peerSearchText.value);
class _PeerWidget extends StatefulWidget { class _PeerWidget extends StatefulWidget {
late final _peers; final Peers peers;
late final OffstageFunc _offstageFunc; final OffstageFunc offstageFunc;
late final PeerCardWidgetFunc _peerCardWidgetFunc; final PeerCardWidgetFunc peerCardWidgetFunc;
_PeerWidget(Peers peers, OffstageFunc offstageFunc, const _PeerWidget(
PeerCardWidgetFunc peerCardWidgetFunc, {required this.peers,
{Key? key}) required this.offstageFunc,
: super(key: key) { required this.peerCardWidgetFunc,
_peers = peers; Key? key})
_offstageFunc = offstageFunc; : super(key: key);
_peerCardWidgetFunc = peerCardWidgetFunc;
}
@override @override
_PeerWidgetState createState() => _PeerWidgetState(); _PeerWidgetState createState() => _PeerWidgetState();
@ -42,9 +40,9 @@ class _PeerWidget extends StatefulWidget {
class _PeerWidgetState extends State<_PeerWidget> with WindowListener { class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
static const int _maxQueryCount = 3; static const int _maxQueryCount = 3;
var _curPeers = Set<String>(); final _curPeers = <String>{};
var _lastChangeTime = DateTime.now(); var _lastChangeTime = DateTime.now();
var _lastQueryPeers = Set<String>(); var _lastQueryPeers = <String>{};
var _lastQueryTime = DateTime.now().subtract(Duration(hours: 1)); var _lastQueryTime = DateTime.now().subtract(Duration(hours: 1));
var _queryCoun = 0; var _queryCoun = 0;
var _exit = false; var _exit = false;
@ -78,65 +76,62 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final space = 12.0; const space = 12.0;
return ChangeNotifierProvider<Peers>( return ChangeNotifierProvider<Peers>(
create: (context) => super.widget._peers, create: (context) => widget.peers,
child: Consumer<Peers>( child: Consumer<Peers>(
builder: (context, peers, child) => peers.peers.isEmpty builder: (context, peers, child) => peers.peers.isEmpty
? Center( ? Center(
child: Text(translate("Empty")), child: Text(translate("Empty")),
) )
: SingleChildScrollView( : SingleChildScrollView(
child: ObxValue<RxString>((searchText) { child: ObxValue<RxString>((searchText) {
return FutureBuilder<List<Peer>>( return FutureBuilder<List<Peer>>(
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
final peers = snapshot.data!; final peers = snapshot.data!;
final cards = <Widget>[]; final cards = <Widget>[];
for (final peer in peers) { for (final peer in peers) {
cards.add(Offstage( cards.add(Offstage(
key: ValueKey("off${peer.id}"), key: ValueKey("off${peer.id}"),
offstage: super.widget._offstageFunc(peer), offstage: widget.offstageFunc(peer),
child: Obx( child: Obx(
() => SizedBox( () => SizedBox(
width: 220, width: 220,
height: height:
peerCardUiType.value == PeerUiType.grid peerCardUiType.value == PeerUiType.grid
? 140 ? 140
: 42, : 42,
child: VisibilityDetector( child: VisibilityDetector(
key: ValueKey(peer.id), key: ValueKey(peer.id),
onVisibilityChanged: (info) { onVisibilityChanged: (info) {
final peerId = final peerId =
(info.key as ValueKey).value; (info.key as ValueKey).value;
if (info.visibleFraction > 0.00001) { if (info.visibleFraction > 0.00001) {
_curPeers.add(peerId); _curPeers.add(peerId);
} else { } else {
_curPeers.remove(peerId); _curPeers.remove(peerId);
} }
_lastChangeTime = DateTime.now(); _lastChangeTime = DateTime.now();
}, },
child: super child: widget.peerCardWidgetFunc(peer),
.widget
._peerCardWidgetFunc(peer),
),
), ),
))); ),
} )));
return Wrap(
spacing: space,
runSpacing: space,
children: cards);
} else {
return const Center(
child: CircularProgressIndicator(),
);
} }
}, return Wrap(
future: matchPeers(searchText.value, peers.peers), spacing: space, runSpacing: space, children: cards);
); } else {
}, peerSearchText), return const Center(
)), child: CircularProgressIndicator(),
);
}
},
future: matchPeers(searchText.value, peers.peers),
);
}, peerSearchText),
),
),
); );
} }
@ -175,31 +170,42 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
} }
abstract class BasePeerWidget extends StatelessWidget { abstract class BasePeerWidget extends StatelessWidget {
late final _name; final String name;
late final _loadEvent; final String loadEvent;
late final OffstageFunc _offstageFunc; final OffstageFunc offstageFunc;
late final PeerCardWidgetFunc _peerCardWidgetFunc; final PeerCardWidgetFunc peerCardWidgetFunc;
late final List<Peer> _initPeers; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _PeerWidget(Peers(_name, _loadEvent, _initPeers), _offstageFunc, return _PeerWidget(
_peerCardWidgetFunc); peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers),
offstageFunc: offstageFunc,
peerCardWidgetFunc: peerCardWidgetFunc);
} }
} }
class RecentPeerWidget extends BasePeerWidget { class RecentPeerWidget extends BasePeerWidget {
RecentPeerWidget({Key? key}) : super(key: key) { RecentPeerWidget({Key? key})
super._name = "recent peer"; : super(
super._loadEvent = "load_recent_peers"; key: key,
super._offstageFunc = (Peer _peer) => false; name: 'recent peer',
super._peerCardWidgetFunc = (Peer peer) => RecentPeerCard( loadEvent: 'load_recent_peers',
peer: peer, offstageFunc: (Peer peer) => false,
peerCardWidgetFunc: (Peer peer) => RecentPeerCard(
peer: peer,
),
initPeers: [],
); );
super._initPeers = [];
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -210,13 +216,17 @@ class RecentPeerWidget extends BasePeerWidget {
} }
class FavoritePeerWidget extends BasePeerWidget { class FavoritePeerWidget extends BasePeerWidget {
FavoritePeerWidget({Key? key}) : super(key: key) { FavoritePeerWidget({Key? key})
super._name = "favorite peer"; : super(
super._loadEvent = "load_fav_peers"; key: key,
super._offstageFunc = (Peer _peer) => false; name: 'favorite peer',
super._peerCardWidgetFunc = (Peer peer) => FavoritePeerCard(peer: peer); loadEvent: 'load_fav_peers',
super._initPeers = []; offstageFunc: (Peer peer) => false,
} peerCardWidgetFunc: (Peer peer) => FavoritePeerCard(
peer: peer,
),
initPeers: [],
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -227,13 +237,17 @@ class FavoritePeerWidget extends BasePeerWidget {
} }
class DiscoveredPeerWidget extends BasePeerWidget { class DiscoveredPeerWidget extends BasePeerWidget {
DiscoveredPeerWidget({Key? key}) : super(key: key) { DiscoveredPeerWidget({Key? key})
super._name = "discovered peer"; : super(
super._loadEvent = "load_lan_peers"; key: key,
super._offstageFunc = (Peer _peer) => false; name: 'discovered peer',
super._peerCardWidgetFunc = (Peer peer) => DiscoveredPeerCard(peer: peer); loadEvent: 'load_lan_peers',
super._initPeers = []; offstageFunc: (Peer peer) => false,
} peerCardWidgetFunc: (Peer peer) => DiscoveredPeerCard(
peer: peer,
),
initPeers: [],
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -244,21 +258,26 @@ class DiscoveredPeerWidget extends BasePeerWidget {
} }
class AddressBookPeerWidget extends BasePeerWidget { class AddressBookPeerWidget extends BasePeerWidget {
AddressBookPeerWidget({Key? key}) : super(key: key) { AddressBookPeerWidget({Key? key})
super._name = "address book peer"; : super(
super._offstageFunc = key: key,
(Peer peer) => !_hitTag(gFFI.abModel.selectedTags, peer.tags); name: 'address book peer',
super._peerCardWidgetFunc = (Peer peer) => AddressBookPeerCard(peer: peer); loadEvent: 'load_address_book_peers',
super._initPeers = _loadPeers(); 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 gFFI.abModel.peers.map((e) {
return Peer.fromJson(e['id'], e); return Peer.fromJson(e['id'], e);
}).toList(); }).toList();
} }
bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) { static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
if (selectedTags.isEmpty) { if (selectedTags.isEmpty) {
return true; 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 'dart:math';
import 'package:desktop_multi_window/desktop_multi_window.dart'; 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/common.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
import 'package:scroll_pos/scroll_pos.dart'; import 'package:scroll_pos/scroll_pos.dart';
import 'package:window_manager/window_manager.dart';
import '../../utils/multi_window_manager.dart'; import '../../utils/multi_window_manager.dart';
@ -33,6 +36,15 @@ class TabInfo {
required this.page}); required this.page});
} }
enum DesktopTabType {
main,
cm,
remoteScreen,
fileTransfer,
portForward,
rdp,
}
class DesktopTabState { class DesktopTabState {
final List<TabInfo> tabs = []; final List<TabInfo> tabs = [];
final ScrollPosController scrollController = final ScrollPosController scrollController =
@ -63,6 +75,7 @@ class DesktopTabController {
state.update((val) { state.update((val) {
val!.tabs.add(tab); val!.tabs.add(tab);
}); });
state.value.scrollController.itemCount = state.value.tabs.length;
toIndex = state.value.tabs.length - 1; toIndex = state.value.tabs.length - 1;
assert(toIndex >= 0); assert(toIndex >= 0);
} }
@ -95,8 +108,16 @@ class DesktopTabController {
void jumpTo(int index) { void jumpTo(int index) {
state.update((val) { state.update((val) {
val!.selected = index; val!.selected = index;
val.pageController.jumpToPage(index); Future.delayed(Duration.zero, (() {
val.scrollController.scrollToItem(index, center: true, animate: true); 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); onSelected?.call(index);
} }
@ -113,11 +134,27 @@ class DesktopTabController {
remove(state.value.selected); 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 { class DesktopTab extends StatelessWidget {
final Function(String)? onTabClose; final Function(String)? onTabClose;
final TarBarTheme theme; final TarBarTheme theme;
final DesktopTabType tabType;
final bool isMainWindow; final bool isMainWindow;
final bool showTabBar; final bool showTabBar;
final bool showLogo; final bool showLogo;
@ -127,23 +164,31 @@ class DesktopTab extends StatelessWidget {
final bool showClose; final bool showClose;
final Widget Function(Widget pageView)? pageViewBuilder; final Widget Function(Widget pageView)? pageViewBuilder;
final Widget? tail; final Widget? tail;
final VoidCallback? onClose;
final TabBuilder? tabBuilder;
final LabelGetter? labelGetter;
final DesktopTabController controller; final DesktopTabController controller;
late final state = controller.state; Rx<DesktopTabState> get state => controller.state;
DesktopTab( const DesktopTab({
{required this.controller, required this.controller,
required this.isMainWindow, required this.tabType,
this.theme = const TarBarTheme.light(), this.theme = const TarBarTheme.light(),
this.onTabClose, this.onTabClose,
this.showTabBar = true, this.showTabBar = true,
this.showLogo = true, this.showLogo = true,
this.showTitle = true, this.showTitle = true,
this.showMinimize = true, this.showMinimize = true,
this.showMaximize = true, this.showMaximize = true,
this.showClose = true, this.showClose = true,
this.pageViewBuilder, this.pageViewBuilder,
this.tail}); this.tail,
this.onClose,
this.tabBuilder,
this.labelGetter,
}) : isMainWindow =
tabType == DesktopTabType.main || tabType == DesktopTabType.cm;
@override @override
Widget build(BuildContext context) { 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() { Widget _buildPageView() {
return Obx(() => PageView( return _buildBlock(
controller: state.value.pageController, child: Obx(() => PageView(
children: controller: state.value.pageController,
state.value.tabs.map((tab) => tab.page).toList(growable: false))); children: state.value.tabs
.map((tab) => tab.page)
.toList(growable: false))));
} }
Widget _buildBar() { Widget _buildBar() {
@ -185,6 +267,11 @@ class DesktopTab extends StatelessWidget {
Expanded( Expanded(
child: Row( child: Row(
children: [ children: [
Offstage(
offstage: !Platform.isMacOS,
child: const SizedBox(
width: 78,
)),
Row(children: [ Row(children: [
Offstage( Offstage(
offstage: !showLogo, offstage: !showLogo,
@ -217,6 +304,8 @@ class DesktopTab extends StatelessWidget {
controller: controller, controller: controller,
onTabClose: onTabClose, onTabClose: onTabClose,
theme: theme, theme: theme,
tabBuilder: tabBuilder,
labelGetter: labelGetter,
)), )),
), ),
], ],
@ -229,6 +318,7 @@ class DesktopTab extends StatelessWidget {
showMinimize: showMinimize, showMinimize: showMinimize,
showMaximize: showMaximize, showMaximize: showMaximize,
showClose: showClose, showClose: showClose,
onClose: onClose,
) )
], ],
); );
@ -242,6 +332,7 @@ class WindowActionPanel extends StatelessWidget {
final bool showMinimize; final bool showMinimize;
final bool showMaximize; final bool showMaximize;
final bool showClose; final bool showClose;
final VoidCallback? onClose;
const WindowActionPanel( const WindowActionPanel(
{Key? key, {Key? key,
@ -249,7 +340,8 @@ class WindowActionPanel extends StatelessWidget {
required this.theme, required this.theme,
this.showMinimize = true, this.showMinimize = true,
this.showMaximize = true, this.showMaximize = true,
this.showClose = true}) this.showClose = true,
this.onClose})
: super(key: key); : super(key: key);
@override @override
@ -323,8 +415,12 @@ class WindowActionPanel extends StatelessWidget {
if (mainTab) { if (mainTab) {
windowManager.close(); windowManager.close();
} else { } 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, is_close: true,
)), )),
@ -336,13 +432,20 @@ class WindowActionPanel extends StatelessWidget {
// ignore: must_be_immutable // ignore: must_be_immutable
class _ListView extends StatelessWidget { class _ListView extends StatelessWidget {
final DesktopTabController controller; final DesktopTabController controller;
late final Rx<DesktopTabState> state;
final Function(String key)? onTabClose; final Function(String key)? onTabClose;
final TarBarTheme theme; final TarBarTheme theme;
final TabBuilder? tabBuilder;
final LabelGetter? labelGetter;
Rx<DesktopTabState> get state => controller.state;
_ListView( _ListView(
{required this.controller, required this.onTabClose, required this.theme}) {required this.controller,
: this.state = controller.state; required this.onTabClose,
required this.theme,
this.tabBuilder,
this.labelGetter});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -356,7 +459,9 @@ class _ListView extends StatelessWidget {
final tab = e.value; final tab = e.value;
return _Tab( return _Tab(
index: index, index: index,
label: tab.label, label: labelGetter == null
? Rx<String>(tab.label)
: labelGetter!(tab.label),
selectedIcon: tab.selectedIcon, selectedIcon: tab.selectedIcon,
unselectedIcon: tab.unselectedIcon, unselectedIcon: tab.unselectedIcon,
closable: tab.closable, closable: tab.closable,
@ -364,22 +469,33 @@ class _ListView extends StatelessWidget {
onClose: () => controller.remove(index), onClose: () => controller.remove(index),
onSelected: () => controller.jumpTo(index), onSelected: () => controller.jumpTo(index),
theme: theme, theme: theme,
tabBuilder: tabBuilder == null
? null
: (Widget icon, Widget labelWidget, TabThemeConf themeConf) {
return tabBuilder!(
tab.label,
icon,
labelWidget,
themeConf,
);
},
); );
}).toList())); }).toList()));
} }
} }
class _Tab extends StatelessWidget { class _Tab extends StatefulWidget {
late final int index; late final int index;
late final String label; late final Rx<String> label;
late final IconData? selectedIcon; late final IconData? selectedIcon;
late final IconData? unselectedIcon; late final IconData? unselectedIcon;
late final bool closable; late final bool closable;
late final int selected; late final int selected;
late final Function() onClose; late final Function() onClose;
late final Function() onSelected; late final Function() onSelected;
final RxBool _hover = false.obs;
late final TarBarTheme theme; late final TarBarTheme theme;
final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)?
tabBuilder;
_Tab( _Tab(
{Key? key, {Key? key,
@ -387,6 +503,7 @@ class _Tab extends StatelessWidget {
required this.label, required this.label,
this.selectedIcon, this.selectedIcon,
this.unselectedIcon, this.unselectedIcon,
this.tabBuilder,
required this.closable, required this.closable,
required this.selected, required this.selected,
required this.onClose, required this.onClose,
@ -394,61 +511,87 @@ class _Tab extends StatelessWidget {
required this.theme}) required this.theme})
: super(key: key); : 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool show_icon = selectedIcon != null && unselectedIcon != null; bool isSelected = widget.index == widget.selected;
bool is_selected = index == selected; bool showDivider =
bool show_divider = index != selected - 1 && index != selected; widget.index != widget.selected - 1 && widget.index != widget.selected;
RxBool hover = restoreHover.value.obs;
return Ink( return Ink(
child: InkWell( child: InkWell(
onHover: (hover) => _hover.value = hover, onHover: (value) {
onTap: () => onSelected(), hover.value = value;
restoreHover.value = value;
},
onTap: () => widget.onSelected(),
child: Row( child: Row(
children: [ children: [
Container( SizedBox(
height: _kTabBarHeight, height: _kTabBarHeight,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Row( _buildTabContent(),
mainAxisAlignment: MainAxisAlignment.center, Obx((() => _CloseButton(
children: [ visiable: hover.value && widget.closable,
Offstage( tabSelected: isSelected,
offstage: !show_icon, onClose: () => widget.onClose(),
child: Icon( theme: widget.theme,
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,
))),
)
])).paddingSymmetric(horizontal: 10), ])).paddingSymmetric(horizontal: 10),
Offstage( Offstage(
offstage: !show_divider, offstage: !showDivider,
child: VerticalDivider( child: VerticalDivider(
width: 1, width: 1,
indent: _kDividerIndent, indent: _kDividerIndent,
endIndent: _kDividerIndent, endIndent: _kDividerIndent,
color: theme.dividerColor, color: widget.theme.dividerColor,
thickness: 1, 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 { class _CloseButton extends StatelessWidget {
@ -480,7 +631,7 @@ class _CloseButton extends StatelessWidget {
child: Offstage( child: Offstage(
offstage: !visiable, offstage: !visiable,
child: InkWell( child: InkWell(
customBorder: RoundedRectangleBorder(), customBorder: const RoundedRectangleBorder(),
onTap: () => onClose(), onTap: () => onClose(),
child: Icon( child: Icon(
Icons.close, 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/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/desktop/pages/server_page.dart';
import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -47,6 +48,9 @@ Future<Null> main(List<String> args) async {
case WindowType.FileTransfer: case WindowType.FileTransfer:
runFileTransferScreen(argument); runFileTransferScreen(argument);
break; break;
case WindowType.PortForward:
runPortForwardScreen(argument);
break;
default: default:
break; break;
} }
@ -76,14 +80,9 @@ Future<void> initEnv(String appType) async {
} }
void runMainApp(bool startService) async { void runMainApp(bool startService) async {
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(1280, 720)); await initEnv(kAppTypeMain);
await Future.wait([ // trigger connection status updater
initEnv(kAppTypeMain), await bind.mainCheckConnectStatus();
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
})
]);
if (startService) { if (startService) {
// await windowManager.ensureInitialized(); // await windowManager.ensureInitialized();
// disable tray // disable tray
@ -91,6 +90,13 @@ void runMainApp(bool startService) async {
gFFI.serverModel.startService(); gFFI.serverModel.startService();
} }
runApp(App()); 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 { 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 { void runConnectionManagerScreen() async {
// initialize window // initialize window
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(300, 400)); WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(300, 400));
@ -182,7 +205,7 @@ class App extends StatelessWidget {
title: 'RustDesk', title: 'RustDesk',
theme: getCurrentTheme(), theme: getCurrentTheme(),
home: isDesktop home: isDesktop
? DesktopTabPage() ? const DesktopTabPage()
: !isAndroid : !isAndroid
? WebHomePage() ? WebHomePage()
: HomePage(), : HomePage(),
@ -190,8 +213,13 @@ class App extends StatelessWidget {
// FirebaseAnalyticsObserver(analytics: analytics), // FirebaseAnalyticsObserver(analytics: analytics),
], ],
builder: isAndroid builder: isAndroid
? (_, child) => AccessibilityListener( ? (context, child) => AccessibilityListener(
child: child, child: MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: 1.0,
),
child: child ?? Container(),
),
) )
: _keepScaleBuilder(), : _keepScaleBuilder(),
), ),

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:ui';
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -35,10 +34,11 @@ class RustDeskMultiWindowManager {
int? _remoteDesktopWindowId; int? _remoteDesktopWindowId;
int? _fileTransferWindowId; int? _fileTransferWindowId;
int? _portForwardWindowId;
Future<dynamic> new_remote_desktop(String remote_id) async { Future<dynamic> newRemoteDesktop(String remoteId) async {
final msg = final msg =
jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remote_id}); jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remoteId});
try { try {
final ids = await DesktopMultiWindow.getAllSubWindowIds(); 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 = final msg =
jsonEncode({"type": WindowType.FileTransfer.index, "id": remote_id}); jsonEncode({"type": WindowType.FileTransfer.index, "id": remoteId});
try { try {
final ids = await DesktopMultiWindow.getAllSubWindowIds(); 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 { Future<dynamic> call(WindowType type, String methodName, dynamic args) async {
int? windowId = findWindowByType(type); int? windowId = findWindowByType(type);
if (windowId == null) { if (windowId == null) {
@ -104,7 +129,7 @@ class RustDeskMultiWindowManager {
case WindowType.FileTransfer: case WindowType.FileTransfer:
return _fileTransferWindowId; return _fileTransferWindowId;
case WindowType.PortForward: case WindowType.PortForward:
break; return _portForwardWindowId;
case WindowType.Unknown: case WindowType.Unknown:
break; break;
} }
@ -120,7 +145,7 @@ class RustDeskMultiWindowManager {
await Future.wait(WindowType.values.map((e) => closeWindows(e))); await Future.wait(WindowType.values.map((e) => closeWindows(e)));
} }
Future<void> closeWindows(WindowType type) async { Future<void> closeWindows(WindowType type) async {
if (type == WindowType.Main) { if (type == WindowType.Main) {
// skip main window, use window manager instead // skip main window, use window manager instead
return; return;

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ use std::{
collections::HashMap, collections::HashMap,
net::SocketAddr, net::SocketAddr,
ops::{Deref, Not}, ops::{Deref, Not},
sync::{mpsc, Arc, Mutex, RwLock}, sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock},
}; };
pub use async_trait::async_trait; pub use async_trait::async_trait;
@ -37,7 +37,6 @@ use hbb_common::{
}; };
pub use helper::LatencyController; pub use helper::LatencyController;
pub use helper::*; pub use helper::*;
use scrap::Image;
use scrap::{ use scrap::{
codec::{Decoder, DecoderCfg}, codec::{Decoder, DecoderCfg},
VpxDecoderConfig, VpxVideoCodecId, VpxDecoderConfig, VpxVideoCodecId,
@ -47,7 +46,12 @@ pub use super::lang::*;
pub mod file_trait; pub mod file_trait;
pub mod helper; 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); pub const SEC30: Duration = Duration::from_secs(30);
/// Client of the remote desktop. /// Client of the remote desktop.
@ -55,7 +59,23 @@ pub struct Client;
#[cfg(not(any(target_os = "android", target_os = "linux")))] #[cfg(not(any(target_os = "android", target_os = "linux")))]
lazy_static::lazy_static! { 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! { cfg_if::cfg_if! {
@ -846,8 +866,7 @@ impl VideoHandler {
#[derive(Default)] #[derive(Default)]
pub struct LoginConfigHandler { pub struct LoginConfigHandler {
id: String, id: String,
pub is_file_transfer: bool, pub conn_type: ConnType,
is_port_forward: bool,
hash: Hash, hash: Hash,
password: Vec<u8>, // remember password for reconnect password: Vec<u8>, // remember password for reconnect
pub remember: bool, pub remember: bool,
@ -886,12 +905,10 @@ impl LoginConfigHandler {
/// # Arguments /// # Arguments
/// ///
/// * `id` - id of peer /// * `id` - id of peer
/// * `is_file_transfer` - Whether the connection is file transfer. /// * `conn_type` - Connection type enum.
/// * `is_port_forward` - Whether the connection is port forward. pub fn initialize(&mut self, id: String, conn_type: ConnType) {
pub fn initialize(&mut self, id: String, is_file_transfer: bool, is_port_forward: bool) {
self.id = id; self.id = id;
self.is_file_transfer = is_file_transfer; self.conn_type = conn_type;
self.is_port_forward = is_port_forward;
let config = self.load_config(); let config = self.load_config();
self.remember = !config.password.is_empty(); self.remember = !config.password.is_empty();
self.config = config; self.config = config;
@ -1048,7 +1065,8 @@ impl LoginConfigHandler {
/// ///
/// * `ignore_default` - If `true`, ignore the default value of the option. /// * `ignore_default` - If `true`, ignore the default value of the option.
fn get_option_message(&self, ignore_default: bool) -> Option<OptionMessage> { 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; return None;
} }
let mut n = 0; let mut n = 0;
@ -1094,7 +1112,8 @@ impl LoginConfigHandler {
} }
pub fn get_option_message_after_login(&self) -> Option<OptionMessage> { 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; return None;
} }
let mut n = 0; let mut n = 0;
@ -1260,13 +1279,13 @@ impl LoginConfigHandler {
/// ///
/// * `username` - The name of the peer. /// * `username` - The name of the peer.
/// * `pi` - The peer info. /// * `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() { if !pi.version.is_empty() {
self.version = hbb_common::get_version_number(&pi.version); 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 { let serde = PeerInfoSerde {
username, username: pi.username.clone(),
hostname: pi.hostname.clone(), hostname: pi.hostname.clone(),
platform: pi.platform.clone(), platform: pi.platform.clone(),
}; };
@ -1330,19 +1349,20 @@ impl LoginConfigHandler {
version: crate::VERSION.to_string(), version: crate::VERSION.to_string(),
..Default::default() ..Default::default()
}; };
if self.is_file_transfer { match self.conn_type {
lr.set_file_transfer(FileTransfer { ConnType::FILE_TRANSFER => lr.set_file_transfer(FileTransfer {
dir: self.get_remote_dir(), dir: self.get_remote_dir(),
show_hidden: !self.get_option("remote_show_hidden").is_empty(), show_hidden: !self.get_option("remote_show_hidden").is_empty(),
..Default::default() ..Default::default()
}); }),
} else if self.is_port_forward { ConnType::PORT_FORWARD => lr.set_port_forward(PortForward {
lr.set_port_forward(PortForward {
host: self.port_forward.0.clone(), host: self.port_forward.0.clone(),
port: self.port_forward.1, port: self.port_forward.1,
..Default::default() ..Default::default()
}); }),
_ => {}
} }
let mut msg_out = Message::new(); let mut msg_out = Message::new();
msg_out.set_login_request(lr); msg_out.set_login_request(lr);
msg_out msg_out
@ -1651,6 +1671,12 @@ pub trait Interface: Send + Clone + 'static + Sized {
fn handle_login_error(&mut self, err: &str) -> bool; fn handle_login_error(&mut self, err: &str) -> bool;
fn handle_peer_info(&mut self, pi: PeerInfo); fn handle_peer_info(&mut self, pi: PeerInfo);
fn set_force_relay(&mut self, direct: bool, received: bool); 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; fn is_force_relay(&self) -> bool;
async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream); 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); 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}; use super::{Data, Interface};
@ -114,4 +114,26 @@ pub trait FileManager: Interface {
fn resume_job(&self, id: i32, is_remote: bool) { fn resume_job(&self, id: i32, is_remote: bool) {
self.send(Data::ResumeJob((id, is_remote))); 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 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::common::make_fd_to_json;
use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; 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::start_server;
use crate::ui_interface; use crate::ui_interface;
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[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::{change_id, check_connect_status, is_ok_change_id};
use crate::ui_interface::{ use crate::ui_interface::{
check_super_user_permission, discover, forget_password, get_api_server, get_app_name, check_mouse_time, check_super_user_permission, discover, forget_password, get_api_server,
get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_langs, get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers,
get_license, get_local_option, get_option, get_options, get_peer, get_peer_option, get_socks, get_langs, get_license, get_local_option, get_mouse_time, get_option, get_options, get_peer,
get_sound_inputs, get_uuid, get_version, has_hwcodec, has_rendezvous_service, post_request, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, has_hwcodec,
set_local_option, set_option, set_options, set_peer_option, set_permanent_password, set_socks, has_rendezvous_service, post_request, set_local_option, set_option, set_options,
store_fav, test_if_valid_server, update_temporary_password, using_public_server, 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) { 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); crate::platform::windows::stop_system_key_propagate(stopped);
} }
pub fn session_connect( // FIXME: -> ResultType<()> cannot be parsed by frb_codegen
events2ui: StreamSink<EventToUI>, // 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
id: String, pub fn session_add_sync(id: String, is_file_transfer: bool, is_port_forward: bool) -> SyncReturn<String> {
is_file_transfer: bool, if let Err(e) = session_add(&id, is_file_transfer, is_port_forward) {
) -> ResultType<()> { SyncReturn(format!("Failed to add session with id {}, {}", &id, e))
Session::start(&id, is_file_transfer, events2ui); } else {
Ok(()) 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> { 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> { pub fn session_get_toggle_option(id: String, arg: String) -> Option<bool> {
if let Some(session) = SESSIONS.read().unwrap().get(&id) { if let Some(session) = SESSIONS.read().unwrap().get(&id) {
Some(session.get_toggle_option(&arg)) Some(session.get_toggle_option(arg))
} else { } else {
None None
} }
@ -137,17 +143,9 @@ pub fn session_get_toggle_option_sync(id: String, arg: String) -> SyncReturn<boo
SyncReturn(res) 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> { pub fn session_get_option(id: String, arg: String) -> Option<String> {
if let Some(session) = SESSIONS.read().unwrap().get(&id) { if let Some(session) = SESSIONS.read().unwrap().get(&id) {
Some(session.get_option(&arg)) Some(session.get_option(arg))
} else { } else {
None 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) { pub fn session_login(id: String, password: String, remember: bool) {
if let Some(session) = SESSIONS.read().unwrap().get(&id) { 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) { pub fn session_refresh(id: String) {
if let Some(session) = SESSIONS.read().unwrap().get(&id) { 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) { 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) { 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) { 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) { 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){ // pub fn session_input_raw_key(id: String, keycode: i32, scancode:i32, down: bool){
if let Some(session) = SESSIONS.read().unwrap().get(&id) { // if let Some(session) = SESSIONS.read().unwrap().get(&id) {
session.input_raw_key(keycode, scancode, down); // session.input_raw_key(keycode, scancode, down);
} // }
} // }
pub fn session_input_key( pub fn session_input_key(
id: String, 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 { pub fn session_get_peer_option(id: String, name: String) -> String {
if let Some(session) = SESSIONS.read().unwrap().get(&id) { if let Some(session) = SESSIONS.read().unwrap().get(&id) {
return session.get_option(&name); return session.get_option(name);
} }
"".to_string() "".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) { pub fn session_load_last_transfer_jobs(id: String) {
if let Some(session) = SESSIONS.read().unwrap().get(&id) { if let Some(session) = SESSIONS.read().unwrap().get(&id) {
return session.load_last_jobs(); // return session.load_last_jobs();
} else { } else {
// a tip for flutter dev // a tip for flutter dev
eprintln!( eprintln!(
@ -473,7 +493,7 @@ pub fn main_get_connect_status() -> String {
pub fn main_check_connect_status() { pub fn main_check_connect_status() {
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[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 { pub fn main_is_using_public_server() -> bool {
@ -598,12 +618,32 @@ pub fn main_load_lan_peers() {
{ {
let data = HashMap::from([ let data = HashMap::from([
("name", "load_lan_peers".to_owned()), ("name", "load_lan_peers".to_owned()),
("peers", serde_json::to_string(&get_lan_peers()).unwrap_or_default()), (
"peers",
serde_json::to_string(&get_lan_peers()).unwrap_or_default(),
),
]); ]);
s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned()));
}; };
} }
pub fn 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 { pub fn main_get_last_remote_id() -> String {
// if !config::APP_DIR.read().unwrap().is_empty() { // if !config::APP_DIR.read().unwrap().is_empty() {
// res = LocalConfig::get_remote_id(); // res = LocalConfig::get_remote_id();
@ -667,7 +707,6 @@ pub fn main_has_hwcodec() -> bool {
has_hwcodec() has_hwcodec()
} }
// TODO
pub fn session_send_mouse(id: String, msg: String) { pub fn session_send_mouse(id: String, msg: String) {
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(&msg) { if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(&msg) {
let alt = m.get("alt").is_some(); let alt = m.get("alt").is_some();
@ -745,6 +784,14 @@ pub fn main_check_super_user_permission() -> bool {
check_super_user_permission() 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) { pub fn cm_send_chat(conn_id: i32, msg: String) {
connection_manager::send_chat(conn_id, msg); 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", "正在重启远程设备"), ("Restarting Remote Device", "正在重启远程设备"),
("remote_restarting_tip", "远程设备正在重启, 请关闭当前提示框, 并在一段时间后使用永久密码重新连接"), ("remote_restarting_tip", "远程设备正在重启, 请关闭当前提示框, 并在一段时间后使用永久密码重新连接"),
("Copied", "已复制"), ("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(); ].iter().cloned().collect();
} }

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", ""), ("Restarting Remote Device", ""),
("remote_restarting_tip", ""), ("remote_restarting_tip", ""),
("Copied", ""), ("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(); ].iter().cloned().collect();
} }

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", ""), ("Restarting Remote Device", ""),
("remote_restarting_tip", ""), ("remote_restarting_tip", ""),
("Copied", ""), ("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(); ].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"), ("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."), ("remote_restarting_tip", "Entferntes Gerät startet neu, bitte schließen Sie diese Meldung und verbinden Sie sich mit dem dauerhaften Passwort erneut."),
("Copied", ""), ("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(); ].iter().cloned().collect();
} }

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", ""), ("Restarting Remote Device", ""),
("remote_restarting_tip", ""), ("remote_restarting_tip", ""),
("Copied", ""), ("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(); ].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"), ("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."), ("remote_restarting_tip", "Dispositivo remoto reiniciando, favor de cerrar este mensaje y reconectarse con la contraseña permamente despues de un momento."),
("Copied", ""), ("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(); ].iter().cloned().collect();
} }

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", ""), ("Restarting Remote Device", ""),
("remote_restarting_tip", ""), ("remote_restarting_tip", ""),
("Copied", ""), ("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(); ].iter().cloned().collect();
} }

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", ""), ("Restarting Remote Device", ""),
("remote_restarting_tip", ""), ("remote_restarting_tip", ""),
("Copied", ""), ("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(); ].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"), ("Restarting Remote Device", "Memulai Ulang Perangkat Jarak Jauh"),
("remote_restarting_tip", ""), ("remote_restarting_tip", ""),
("Copied", ""), ("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(); ].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?"), ("Are you sure you want to restart", "Sei sicuro di voler riavviare?"),
("Restarting Remote Device", "Il dispositivo remoto si sta riavviando"), ("Restarting Remote Device", "Il dispositivo remoto si sta riavviando"),
("remote_restarting_tip", "Riavviare il dispositivo remoto"), ("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", ""), ("Legacy mode", ""),
("Map mode", ""), ("Map mode", ""),
("Translate 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", "本当に再起動しますか"), ("Are you sure you want to restart", "本当に再起動しますか"),
("Restarting Remote Device", "リモート端末を再起動中"), ("Restarting Remote Device", "リモート端末を再起動中"),
("remote_restarting_tip", "リモート端末は再起動中です。このメッセージボックスを閉じて、しばらくした後に固定のパスワードを使用して再接続してください。"), ("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(); ].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", "정말로 재시작 하시겠습니까"), ("Are you sure you want to restart", "정말로 재시작 하시겠습니까"),
("Restarting Remote Device", "원격 기기를 다시 시작하는중"), ("Restarting Remote Device", "원격 기기를 다시 시작하는중"),
("remote_restarting_tip", "원격 장치를 다시 시작하는 중입니다. 이 메시지 상자를 닫고 잠시 후 영구 비밀번호로 다시 연결하십시오."), ("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(); ].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"), ("Set security password", "Ustaw hasło zabezpieczające"),
("Connection not allowed", "Połączenie niedozwolone"), ("Connection not allowed", "Połączenie niedozwolone"),
("Copied", ""), ("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(); ].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"), ("Are you sure you want to restart", "Tem a certeza que pretende reiniciar"),
("Restarting Remote Device", "A reiniciar sistema remoto"), ("Restarting Remote Device", "A reiniciar sistema remoto"),
("remote_restarting_tip", ""), ("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(); ].iter().cloned().collect();
} }

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", ""), ("Restarting Remote Device", ""),
("remote_restarting_tip", ""), ("remote_restarting_tip", ""),
("Copied", ""), ("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(); ].iter().cloned().collect();
} }

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", "Перезагрузка удаленного устройства"), ("Restarting Remote Device", "Перезагрузка удаленного устройства"),
("remote_restarting_tip", "Удаленное устройство перезапускается. Пожалуйста, закройте это сообщение и через некоторое время переподключитесь, используя постоянный пароль."), ("remote_restarting_tip", "Удаленное устройство перезапускается. Пожалуйста, закройте это сообщение и через некоторое время переподключитесь, используя постоянный пароль."),
("Copied", ""), ("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(); ].iter().cloned().collect();
} }

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", ""), ("Restarting Remote Device", ""),
("remote_restarting_tip", ""), ("remote_restarting_tip", ""),
("Copied", ""), ("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(); ].iter().cloned().collect();
} }

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", ""), ("Restarting Remote Device", ""),
("remote_restarting_tip", ""), ("remote_restarting_tip", ""),
("Copied", ""), ("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(); ].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"), ("Restarting Remote Device", "Uzaktan yeniden başlatılıyor"),
("remote_restarting_tip", ""), ("remote_restarting_tip", ""),
("Copied", ""), ("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(); ].iter().cloned().collect();
} }

View File

@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restarting Remote Device", "正在重啓遠程設備"), ("Restarting Remote Device", "正在重啓遠程設備"),
("remote_restarting_tip", "遠程設備正在重啓,請關閉當前提示框,並在一段時間後使用永久密碼重新連接"), ("remote_restarting_tip", "遠程設備正在重啓,請關閉當前提示框,並在一段時間後使用永久密碼重新連接"),
("Copied", "已複製"), ("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(); ].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"), ("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"), ("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", ""), ("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(); ].iter().cloned().collect();
} }

View File

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

View File

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

View File

@ -950,6 +950,7 @@ impl Connection {
addr addr
)) ))
.await; .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(); let args: Vec<String> = iter.map(|x| x.clone()).collect();
frame.set_title(&id); frame.set_title(&id);
frame.register_behavior("native-remote", move || { frame.register_behavior("native-remote", move || {
Box::new(remote::Handler::new( Box::new(remote::SciterSession::new(
cmd.clone(), cmd.clone(),
id.clone(), id.clone(),
pass.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