mirror of
https://github.com/rustdesk/rustdesk.git
synced 2024-12-03 19:39:20 +08:00
Merge branch 'master' of https://github.com/rustdesk/rustdesk
This commit is contained in:
commit
bc7611ae0d
@ -32,10 +32,10 @@ RustDesk welcomes contribution from everyone. See [`CONTRIBUTING.md`](CONTRIBUTI
|
||||
Below are the servers you are using for free, it may change along the time. If you are not close to one of these, your network may be slow.
|
||||
| Location | Vendor | Specification |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM |
|
||||
| Singapore | Vultr | 1 VCPU / 1GB RAM |
|
||||
| Germany | Hetzner | 2 VCPU / 4GB RAM |
|
||||
| Germany | Codext | 4 VCPU / 8GB RAM |
|
||||
| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
|
||||
| Singapore | Vultr | 1 vCPU / 1GB RAM |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Germany | Codext | 4 vCPU / 8GB RAM |
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
2
flutter/.gitignore
vendored
2
flutter/.gitignore
vendored
@ -45,7 +45,7 @@ jniLibs
|
||||
# flutter rust bridge
|
||||
|
||||
# Flutter Generated Files
|
||||
**/flutter/GeneratedPluginRegistrant.swift
|
||||
**/GeneratedPluginRegistrant.swift
|
||||
**/flutter/generated_plugin_registrant.cc
|
||||
**/flutter/generated_plugin_registrant.h
|
||||
**/flutter/generated_plugins.cmake
|
||||
|
@ -427,7 +427,45 @@ class CustomAlertDialog extends StatelessWidget {
|
||||
void msgBox(
|
||||
String type, String title, String text, OverlayDialogManager dialogManager,
|
||||
{bool? hasCancel}) {
|
||||
var wrap = (String text, void Function() onPressed) => ButtonTheme(
|
||||
dialogManager.dismissAll();
|
||||
List<Widget> buttons = [];
|
||||
if (type != "connecting" && type != "success" && !type.contains("nook")) {
|
||||
buttons.insert(
|
||||
0,
|
||||
msgBoxButton(translate('OK'), () {
|
||||
dialogManager.dismissAll();
|
||||
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
||||
if (!type.contains("custom")) {
|
||||
closeConnection();
|
||||
}
|
||||
}));
|
||||
}
|
||||
hasCancel ??= !type.contains("error") &&
|
||||
!type.contains("nocancel") &&
|
||||
type != "restarting";
|
||||
if (hasCancel) {
|
||||
buttons.insert(
|
||||
0,
|
||||
msgBoxButton(translate('Cancel'), () {
|
||||
dialogManager.dismissAll();
|
||||
}));
|
||||
}
|
||||
// TODO: test this button
|
||||
if (type.contains("hasclose")) {
|
||||
buttons.insert(
|
||||
0,
|
||||
msgBoxButton(translate('Close'), () {
|
||||
dialogManager.dismissAll();
|
||||
}));
|
||||
}
|
||||
dialogManager.show((setState, close) => CustomAlertDialog(
|
||||
title: _msgBoxTitle(title),
|
||||
content: Text(translate(text), style: TextStyle(fontSize: 15)),
|
||||
actions: buttons));
|
||||
}
|
||||
|
||||
Widget msgBoxButton(String text, void Function() onPressed) {
|
||||
return ButtonTheme(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
//limits the touch area to the button area
|
||||
@ -439,41 +477,16 @@ void msgBox(
|
||||
onPressed: onPressed,
|
||||
child:
|
||||
Text(translate(text), style: TextStyle(color: MyTheme.accent))));
|
||||
}
|
||||
|
||||
Widget _msgBoxTitle(String title) => Text(translate(title), style: TextStyle(fontSize: 21));
|
||||
|
||||
void msgBoxCommon(OverlayDialogManager dialogManager, String title,
|
||||
Widget content, List<Widget> buttons) {
|
||||
dialogManager.dismissAll();
|
||||
List<Widget> buttons = [];
|
||||
if (type != "connecting" && type != "success" && type.indexOf("nook") < 0) {
|
||||
buttons.insert(
|
||||
0,
|
||||
wrap(translate('OK'), () {
|
||||
dialogManager.dismissAll();
|
||||
closeConnection();
|
||||
}));
|
||||
}
|
||||
if (hasCancel == null) {
|
||||
// hasCancel = type != 'error';
|
||||
hasCancel = type.indexOf("error") < 0 &&
|
||||
type.indexOf("nocancel") < 0 &&
|
||||
type != "restarting";
|
||||
}
|
||||
if (hasCancel) {
|
||||
buttons.insert(
|
||||
0,
|
||||
wrap(translate('Cancel'), () {
|
||||
dialogManager.dismissAll();
|
||||
}));
|
||||
}
|
||||
// TODO: test this button
|
||||
if (type.indexOf("hasclose") >= 0) {
|
||||
buttons.insert(
|
||||
0,
|
||||
wrap(translate('Close'), () {
|
||||
dialogManager.dismissAll();
|
||||
}));
|
||||
}
|
||||
dialogManager.show((setState, close) => CustomAlertDialog(
|
||||
title: Text(translate(title), style: TextStyle(fontSize: 21)),
|
||||
content: Text(translate(text), style: TextStyle(fontSize: 15)),
|
||||
title: _msgBoxTitle(title),
|
||||
content: content,
|
||||
actions: buttons));
|
||||
}
|
||||
|
||||
@ -492,13 +505,13 @@ const G = M * K;
|
||||
|
||||
String readableFileSize(double size) {
|
||||
if (size < K) {
|
||||
return size.toStringAsFixed(2) + " B";
|
||||
return "${size.toStringAsFixed(2)} B";
|
||||
} else if (size < M) {
|
||||
return (size / K).toStringAsFixed(2) + " KB";
|
||||
return "${(size / K).toStringAsFixed(2)} KB";
|
||||
} else if (size < G) {
|
||||
return (size / M).toStringAsFixed(2) + " MB";
|
||||
return "${(size / M).toStringAsFixed(2)} MB";
|
||||
} else {
|
||||
return (size / G).toStringAsFixed(2) + " GB";
|
||||
return "${(size / G).toStringAsFixed(2)} GB";
|
||||
}
|
||||
}
|
||||
|
||||
@ -661,8 +674,6 @@ Future<void> initGlobalFFI() async {
|
||||
debugPrint("_globalFFI init end");
|
||||
// after `put`, can also be globally found by Get.find<FFI>();
|
||||
Get.put(_globalFFI, permanent: true);
|
||||
// trigger connection status updater
|
||||
await bind.mainCheckConnectStatus();
|
||||
// global shared preference
|
||||
await Get.putAsync(() => SharedPreferences.getInstance());
|
||||
}
|
||||
|
87
flutter/lib/common/shared_state.dart
Normal file
87
flutter/lib/common/shared_state.dart
Normal 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));
|
||||
}
|
@ -4,8 +4,14 @@ const double kDesktopRemoteTabBarHeight = 28.0;
|
||||
const String kAppTypeMain = "main";
|
||||
const String kAppTypeDesktopRemote = "remote";
|
||||
const String kAppTypeDesktopFileTransfer = "file transfer";
|
||||
const String kAppTypeDesktopPortForward = "port forward";
|
||||
const String kTabLabelHomePage = "Home";
|
||||
const String kTabLabelSettingPage = "Settings";
|
||||
|
||||
const int kDefaultDisplayWidth = 1280;
|
||||
const int kDefaultDisplayHeight = 720;
|
||||
const int kMobileDefaultDisplayWidth = 720;
|
||||
const int kMobileDefaultDisplayHeight = 1280;
|
||||
|
||||
const int kDesktopDefaultDisplayWidth = 1080;
|
||||
const int kDesktopDefaultDisplayHeight = 720;
|
||||
|
||||
const kInvalidValueStr = "InvalidValueStr";
|
||||
|
@ -33,7 +33,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
final _idController = TextEditingController();
|
||||
|
||||
/// Update url. If it's not null, means an update is available.
|
||||
var _updateUrl = '';
|
||||
final _updateUrl = '';
|
||||
|
||||
Timer? _updateTimer;
|
||||
|
||||
@ -92,7 +92,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
if (snapshot.hasData) {
|
||||
return snapshot.data!;
|
||||
} else {
|
||||
return Offstage();
|
||||
return const Offstage();
|
||||
}
|
||||
}),
|
||||
],
|
||||
@ -110,7 +110,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
/// Callback for the connect button.
|
||||
/// Connects to the selected peer.
|
||||
void onConnect({bool isFileTransfer = false}) {
|
||||
var id = _idController.text.trim();
|
||||
final id = _idController.text.trim();
|
||||
connect(id, isFileTransfer: isFileTransfer);
|
||||
}
|
||||
|
||||
@ -120,9 +120,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
if (id == '') return;
|
||||
id = id.replaceAll(' ', '');
|
||||
if (isFileTransfer) {
|
||||
await rustDeskWinManager.new_file_transfer(id);
|
||||
await rustDeskWinManager.newFileTransfer(id);
|
||||
} else {
|
||||
await rustDeskWinManager.new_remote_desktop(id);
|
||||
await rustDeskWinManager.newRemoteDesktop(id);
|
||||
}
|
||||
FocusScopeNode currentFocus = FocusScope.of(context);
|
||||
if (!currentFocus.hasPrimaryFocus) {
|
||||
@ -233,7 +233,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
},
|
||||
child: Container(
|
||||
height: 24,
|
||||
width: 72,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: ftPressed.value
|
||||
@ -257,7 +256,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
color: ftPressed.value
|
||||
? MyTheme.color(context).bg
|
||||
: MyTheme.color(context).text),
|
||||
),
|
||||
).marginSymmetric(horizontal: 12),
|
||||
),
|
||||
)),
|
||||
SizedBox(
|
||||
@ -272,7 +271,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
onTap: onConnect,
|
||||
child: Container(
|
||||
height: 24,
|
||||
width: 65,
|
||||
decoration: BoxDecoration(
|
||||
color: connPressed.value
|
||||
? MyTheme.accent
|
||||
@ -289,12 +287,12 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
child: Center(
|
||||
child: Text(
|
||||
translate(
|
||||
"Connection",
|
||||
"Connect",
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: MyTheme.color(context).bg),
|
||||
),
|
||||
),
|
||||
).marginSymmetric(horizontal: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -3,14 +3,13 @@ import 'dart:convert';
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../models/model.dart';
|
||||
|
||||
class ConnectionTabPage extends StatefulWidget {
|
||||
final Map<String, dynamic> params;
|
||||
|
||||
@ -22,26 +21,27 @@ class ConnectionTabPage extends StatefulWidget {
|
||||
|
||||
class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
final tabController = Get.put(DesktopTabController());
|
||||
static final Rx<String> _fullscreenID = "".obs;
|
||||
static final IconData selectedIcon = Icons.desktop_windows_sharp;
|
||||
static final IconData unselectedIcon = Icons.desktop_windows_outlined;
|
||||
static const IconData selectedIcon = Icons.desktop_windows_sharp;
|
||||
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
|
||||
|
||||
var connectionMap = RxList<Widget>.empty(growable: true);
|
||||
|
||||
_ConnectionTabPageState(Map<String, dynamic> params) {
|
||||
if (params['id'] != null) {
|
||||
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
||||
final peerId = params['id'];
|
||||
if (peerId != null) {
|
||||
ConnectionTypeState.init(peerId);
|
||||
tabController.add(TabInfo(
|
||||
key: params['id'],
|
||||
label: params['id'],
|
||||
key: peerId,
|
||||
label: peerId,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
page: RemotePage(
|
||||
key: ValueKey(params['id']),
|
||||
id: params['id'],
|
||||
tabBarHeight:
|
||||
_fullscreenID.value.isNotEmpty ? 0 : kDesktopRemoteTabBarHeight,
|
||||
fullscreenID: _fullscreenID,
|
||||
)));
|
||||
page: Obx(() => RemotePage(
|
||||
key: ValueKey(peerId),
|
||||
id: peerId,
|
||||
tabBarHeight:
|
||||
fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight,
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,33 +54,27 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
print(
|
||||
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
|
||||
|
||||
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
||||
// for simplify, just replace connectionId
|
||||
if (call.method == "new_remote_desktop") {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
window_on_top(windowId());
|
||||
ConnectionTypeState.init(id);
|
||||
tabController.add(TabInfo(
|
||||
key: id,
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
page: RemotePage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
tabBarHeight: _fullscreenID.value.isNotEmpty
|
||||
? 0
|
||||
: kDesktopRemoteTabBarHeight,
|
||||
fullscreenID: _fullscreenID,
|
||||
)));
|
||||
page: Obx(() => RemotePage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
tabBarHeight:
|
||||
fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight,
|
||||
))));
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.state.value.tabs.forEach((tab) {
|
||||
print("executing onDestroy hook, closing ${tab.label}}");
|
||||
final tag = tab.label;
|
||||
ffi(tag).close().then((_) {
|
||||
Get.delete<FFI>(tag: tag);
|
||||
});
|
||||
});
|
||||
Get.back();
|
||||
tabController.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -88,36 +82,79 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
|
||||
return SubWindowDragToResizeArea(
|
||||
windowId: windowId(),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: Obx(() => DesktopTab(
|
||||
controller: tabController,
|
||||
theme: theme,
|
||||
isMainWindow: false,
|
||||
showTabBar: _fullscreenID.value.isEmpty,
|
||||
tail: AddButton(
|
||||
theme: theme,
|
||||
).paddingOnly(left: 10),
|
||||
pageViewBuilder: (pageView) {
|
||||
WindowController.fromWindowId(windowId())
|
||||
.setFullscreen(_fullscreenID.value.isNotEmpty);
|
||||
return pageView;
|
||||
},
|
||||
))),
|
||||
),
|
||||
);
|
||||
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
||||
return Obx(() => SubWindowDragToResizeArea(
|
||||
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
|
||||
windowId: windowId(),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: Obx(() => DesktopTab(
|
||||
controller: tabController,
|
||||
theme: theme,
|
||||
tabType: DesktopTabType.remoteScreen,
|
||||
showTabBar: fullscreen.isFalse,
|
||||
onClose: () {
|
||||
tabController.clear();
|
||||
},
|
||||
tail: AddButton(
|
||||
theme: theme,
|
||||
).paddingOnly(left: 10),
|
||||
pageViewBuilder: (pageView) {
|
||||
WindowController.fromWindowId(windowId())
|
||||
.setFullscreen(fullscreen.isTrue);
|
||||
return pageView;
|
||||
},
|
||||
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
||||
final connectionType = ConnectionTypeState.find(key);
|
||||
if (!connectionType.isValid()) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
label,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
final msgDirect = translate(
|
||||
connectionType.direct.value ==
|
||||
ConnectionType.strDirect
|
||||
? 'Direct Connection'
|
||||
: 'Relay Connection');
|
||||
final msgSecure = translate(
|
||||
connectionType.secure.value ==
|
||||
ConnectionType.strSecure
|
||||
? 'Secure Connection'
|
||||
: 'Insecure Connection');
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
Tooltip(
|
||||
message: '$msgDirect\n$msgSecure',
|
||||
child: Image.asset(
|
||||
'assets/${connectionType.secure.value}${connectionType.direct.value}.png',
|
||||
width: themeConf.iconSize,
|
||||
height: themeConf.iconSize,
|
||||
).paddingOnly(right: 5),
|
||||
),
|
||||
label,
|
||||
],
|
||||
);
|
||||
}
|
||||
}),
|
||||
))),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
void onRemoveId(String id) {
|
||||
ffi(id).close();
|
||||
if (tabController.state.value.tabs.length == 0) {
|
||||
WindowController.fromWindowId(windowId()).close();
|
||||
if (tabController.state.value.tabs.isEmpty) {
|
||||
WindowController.fromWindowId(windowId()).hide();
|
||||
}
|
||||
ConnectionTypeState.delete(id);
|
||||
}
|
||||
|
||||
int windowId() {
|
||||
|
@ -806,6 +806,8 @@ Future<bool> loginDialog() async {
|
||||
var userNameMsg = "";
|
||||
String pass = "";
|
||||
var passMsg = "";
|
||||
var userContontroller = TextEditingController(text: userName);
|
||||
var pwdController = TextEditingController(text: pass);
|
||||
|
||||
var isInProgress = false;
|
||||
var completer = Completer<bool>();
|
||||
@ -833,13 +835,10 @@ Future<bool> loginDialog() async {
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (s) {
|
||||
userName = s;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
errorText: userNameMsg.isNotEmpty ? userNameMsg : null),
|
||||
controller: TextEditingController(text: userName),
|
||||
controller: userContontroller,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -859,13 +858,10 @@ Future<bool> loginDialog() async {
|
||||
Expanded(
|
||||
child: TextField(
|
||||
obscureText: true,
|
||||
onChanged: (s) {
|
||||
pass = s;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
errorText: passMsg.isNotEmpty ? passMsg : null),
|
||||
controller: TextEditingController(text: pass),
|
||||
controller: pwdController,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -896,8 +892,8 @@ Future<bool> loginDialog() async {
|
||||
isInProgress = false;
|
||||
});
|
||||
};
|
||||
userName = userName;
|
||||
pass = pass;
|
||||
userName = userContontroller.text;
|
||||
pass = pwdController.text;
|
||||
if (userName.isEmpty) {
|
||||
userNameMsg = translate("Username missed");
|
||||
cancel();
|
||||
|
@ -1025,7 +1025,6 @@ class _ComboBox extends StatelessWidget {
|
||||
|
||||
void changeServer() async {
|
||||
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
||||
print("${oldOptions}");
|
||||
String idServer = oldOptions['custom-rendezvous-server'] ?? "";
|
||||
var idServerMsg = "";
|
||||
String relayServer = oldOptions['relay-server'] ?? "";
|
||||
@ -1033,6 +1032,10 @@ void changeServer() async {
|
||||
String apiServer = oldOptions['api-server'] ?? "";
|
||||
var apiServerMsg = "";
|
||||
var key = oldOptions['key'] ?? "";
|
||||
var idController = TextEditingController(text: idServer);
|
||||
var relayController = TextEditingController(text: relayServer);
|
||||
var apiController = TextEditingController(text: apiServer);
|
||||
var keyController = TextEditingController(text: key);
|
||||
|
||||
var isInProgress = false;
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
@ -1057,13 +1060,10 @@ void changeServer() async {
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (s) {
|
||||
idServer = s;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
errorText: idServerMsg.isNotEmpty ? idServerMsg : null),
|
||||
controller: TextEditingController(text: idServer),
|
||||
controller: idController,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -1082,14 +1082,11 @@ void changeServer() async {
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (s) {
|
||||
relayServer = s;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
errorText:
|
||||
relayServerMsg.isNotEmpty ? relayServerMsg : null),
|
||||
controller: TextEditingController(text: relayServer),
|
||||
controller: relayController,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -1108,14 +1105,11 @@ void changeServer() async {
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (s) {
|
||||
apiServer = s;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
errorText:
|
||||
apiServerMsg.isNotEmpty ? apiServerMsg : null),
|
||||
controller: TextEditingController(text: apiServer),
|
||||
controller: apiController,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -1134,13 +1128,10 @@ void changeServer() async {
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (s) {
|
||||
key = s;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(text: key),
|
||||
controller: keyController,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -1171,10 +1162,10 @@ void changeServer() async {
|
||||
isInProgress = false;
|
||||
});
|
||||
};
|
||||
idServer = idServer.trim();
|
||||
relayServer = relayServer.trim();
|
||||
apiServer = apiServer.trim();
|
||||
key = key.trim();
|
||||
idServer = idController.text.trim();
|
||||
relayServer = relayController.text.trim();
|
||||
apiServer = apiController.text.trim().toLowerCase();
|
||||
key = keyController.text.trim();
|
||||
|
||||
if (idServer.isNotEmpty) {
|
||||
idServerMsg = translate(
|
||||
@ -1230,6 +1221,7 @@ void changeWhiteList() async {
|
||||
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
||||
var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(',');
|
||||
var newWhiteListField = newWhiteList.join('\n');
|
||||
var controller = TextEditingController(text: newWhiteListField);
|
||||
var msg = "";
|
||||
var isInProgress = false;
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
@ -1246,15 +1238,12 @@ void changeWhiteList() async {
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (s) {
|
||||
newWhiteListField = s;
|
||||
},
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
errorText: msg.isEmpty ? null : translate(msg),
|
||||
),
|
||||
controller: TextEditingController(text: newWhiteListField),
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -1277,7 +1266,7 @@ void changeWhiteList() async {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
newWhiteListField = newWhiteListField.trim();
|
||||
newWhiteListField = controller.text.trim();
|
||||
var newWhiteList = "";
|
||||
if (newWhiteListField.isEmpty) {
|
||||
// pass
|
||||
@ -1319,6 +1308,9 @@ void changeSocks5Proxy() async {
|
||||
username = socks[1];
|
||||
password = socks[2];
|
||||
}
|
||||
var proxyController = TextEditingController(text: proxy);
|
||||
var userController = TextEditingController(text: username);
|
||||
var pwdController = TextEditingController(text: password);
|
||||
|
||||
var isInProgress = false;
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
@ -1343,13 +1335,10 @@ void changeSocks5Proxy() async {
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (s) {
|
||||
proxy = s;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
|
||||
controller: TextEditingController(text: proxy),
|
||||
controller: proxyController,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -1368,13 +1357,10 @@ void changeSocks5Proxy() async {
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (s) {
|
||||
username = s;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(text: username),
|
||||
controller: userController,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -1393,13 +1379,10 @@ void changeSocks5Proxy() async {
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (s) {
|
||||
password = s;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(text: password),
|
||||
controller: pwdController,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -1428,9 +1411,9 @@ void changeSocks5Proxy() async {
|
||||
isInProgress = false;
|
||||
});
|
||||
};
|
||||
proxy = proxy.trim();
|
||||
username = username.trim();
|
||||
password = password.trim();
|
||||
proxy = proxyController.text.trim();
|
||||
username = userController.text.trim();
|
||||
password = pwdController.text.trim();
|
||||
|
||||
if (proxy.isNotEmpty) {
|
||||
proxyMsg =
|
||||
|
@ -4,6 +4,7 @@ import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class DesktopTabPage extends StatefulWidget {
|
||||
@ -33,26 +34,29 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dark = isDarkTheme();
|
||||
return DragToResizeArea(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||
isMainWindow: true,
|
||||
tail: ActionIcon(
|
||||
message: 'Settings',
|
||||
icon: IconFont.menu,
|
||||
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||
onTap: onAddSetting,
|
||||
is_close: false,
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
RxBool fullscreen = false.obs;
|
||||
Get.put(fullscreen, tag: 'fullscreen');
|
||||
return Obx(() => DragToResizeArea(
|
||||
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||
tabType: DesktopTabType.main,
|
||||
tail: ActionIcon(
|
||||
message: 'Settings',
|
||||
icon: IconFont.menu,
|
||||
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||
onTap: onAddSetting,
|
||||
is_close: false,
|
||||
),
|
||||
)),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
void onAddSetting() {
|
||||
|
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/file_manager_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
@ -20,12 +19,13 @@ class FileManagerTabPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
final tabController = Get.put(DesktopTabController());
|
||||
DesktopTabController get tabController => Get.find<DesktopTabController>();
|
||||
|
||||
static final IconData selectedIcon = Icons.file_copy_sharp;
|
||||
static final IconData unselectedIcon = Icons.file_copy_outlined;
|
||||
|
||||
_FileManagerTabPageState(Map<String, dynamic> params) {
|
||||
Get.put(DesktopTabController());
|
||||
tabController.add(TabInfo(
|
||||
key: params['id'],
|
||||
label: params['id'],
|
||||
@ -42,7 +42,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
print(
|
||||
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
|
||||
"call ${call.method} with args ${call.arguments} from window ${fromWindowId} to ${windowId()}");
|
||||
// for simplify, just replace connectionId
|
||||
if (call.method == "new_file_transfer") {
|
||||
final args = jsonDecode(call.arguments);
|
||||
@ -55,21 +55,15 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
unselectedIcon: unselectedIcon,
|
||||
page: FileManagerPage(key: ValueKey(id), id: id)));
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.state.value.tabs.forEach((tab) {
|
||||
print("executing onDestroy hook, closing ${tab.label}}");
|
||||
final tag = tab.label;
|
||||
ffi(tag).close().then((_) {
|
||||
Get.delete<FFI>(tag: tag);
|
||||
});
|
||||
});
|
||||
Get.back();
|
||||
tabController.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
|
||||
final theme =
|
||||
isDarkTheme() ? const TarBarTheme.dark() : const TarBarTheme.light();
|
||||
return SubWindowDragToResizeArea(
|
||||
windowId: windowId(),
|
||||
child: Container(
|
||||
@ -80,7 +74,10 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
theme: theme,
|
||||
isMainWindow: false,
|
||||
tabType: DesktopTabType.fileTransfer,
|
||||
onClose: () {
|
||||
tabController.clear();
|
||||
},
|
||||
tail: AddButton(
|
||||
theme: theme,
|
||||
).paddingOnly(left: 10),
|
||||
@ -90,9 +87,8 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
}
|
||||
|
||||
void onRemoveId(String id) {
|
||||
ffi("ft_$id").close();
|
||||
if (tabController.state.value.tabs.length == 0) {
|
||||
WindowController.fromWindowId(windowId()).close();
|
||||
if (tabController.state.value.tabs.isEmpty) {
|
||||
WindowController.fromWindowId(windowId()).hide();
|
||||
}
|
||||
}
|
||||
|
||||
|
348
flutter/lib/desktop/pages/port_forward_page.dart
Normal file
348
flutter/lib/desktop/pages/port_forward_page.dart
Normal file
@ -0,0 +1,348 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
|
||||
const double _kColumn1Width = 30;
|
||||
const double _kColumn4Width = 100;
|
||||
const double _kRowHeight = 50;
|
||||
const double _kTextLeftMargin = 20;
|
||||
|
||||
class _PortForward {
|
||||
int localPort;
|
||||
String remoteHost;
|
||||
int remotePort;
|
||||
|
||||
_PortForward.fromJson(List<dynamic> json)
|
||||
: localPort = json[0] as int,
|
||||
remoteHost = json[1] as String,
|
||||
remotePort = json[2] as int;
|
||||
}
|
||||
|
||||
class PortForwardPage extends StatefulWidget {
|
||||
const PortForwardPage({Key? key, required this.id, required this.isRDP})
|
||||
: super(key: key);
|
||||
final String id;
|
||||
final bool isRDP;
|
||||
|
||||
@override
|
||||
State<PortForwardPage> createState() => _PortForwardPageState();
|
||||
}
|
||||
|
||||
class _PortForwardPageState extends State<PortForwardPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final bool isRdp = false;
|
||||
final TextEditingController localPortController = TextEditingController();
|
||||
final TextEditingController remoteHostController = TextEditingController();
|
||||
final TextEditingController remotePortController = TextEditingController();
|
||||
RxList<_PortForward> pfs = RxList.empty(growable: true);
|
||||
late FFI _ffi;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ffi = FFI();
|
||||
_ffi.connect(widget.id, isPortForward: true);
|
||||
Get.put(_ffi, tag: 'pf_${widget.id}');
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.enable();
|
||||
}
|
||||
print("init success with id ${widget.id}");
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ffi.close();
|
||||
_ffi.dialogManager.dismissAll();
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.disable();
|
||||
}
|
||||
Get.delete<FFI>(tag: 'pf_${widget.id}');
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
backgroundColor: MyTheme.color(context).grayBg,
|
||||
body: FutureBuilder(future: () async {
|
||||
if (!isRdp) {
|
||||
refreshTunnelConfig();
|
||||
}
|
||||
}(), builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 20, color: MyTheme.color(context).grayBg!)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
buildPrompt(context),
|
||||
Flexible(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: MyTheme.color(context).bg,
|
||||
border: Border.all(width: 1, color: MyTheme.border)),
|
||||
child:
|
||||
widget.isRDP ? buildRdp(context) : buildTunnel(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Offstage();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
buildPrompt(BuildContext context) {
|
||||
return Obx(() => Offstage(
|
||||
offstage: pfs.isEmpty && !widget.isRDP,
|
||||
child: Container(
|
||||
height: 45,
|
||||
color: const Color(0xFF007F00),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translate('Listening ...'),
|
||||
style: const TextStyle(fontSize: 16, color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
translate('not_close_tcp_tip'),
|
||||
style: const TextStyle(
|
||||
fontSize: 10, color: Color(0xFFDDDDDD), height: 1.2),
|
||||
)
|
||||
])).marginOnly(bottom: 8),
|
||||
));
|
||||
}
|
||||
|
||||
buildTunnel(BuildContext context) {
|
||||
text(String lable) => Expanded(
|
||||
child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin));
|
||||
|
||||
return Theme(
|
||||
data: Theme.of(context)
|
||||
.copyWith(backgroundColor: MyTheme.color(context).bg),
|
||||
child: Obx(() => ListView.builder(
|
||||
itemCount: pfs.length + 2,
|
||||
itemBuilder: ((context, index) {
|
||||
if (index == 0) {
|
||||
return Container(
|
||||
height: 25,
|
||||
color: MyTheme.color(context).grayBg,
|
||||
child: Row(children: [
|
||||
text('Local Port'),
|
||||
const SizedBox(width: _kColumn1Width),
|
||||
text('Remote Host'),
|
||||
text('Remote Port'),
|
||||
SizedBox(
|
||||
width: _kColumn4Width, child: Text(translate('Action')))
|
||||
]),
|
||||
);
|
||||
} else if (index == 1) {
|
||||
return buildTunnelAddRow(context);
|
||||
} else {
|
||||
return buildTunnelDataRow(context, pfs[index - 2], index - 2);
|
||||
}
|
||||
}))),
|
||||
);
|
||||
}
|
||||
|
||||
buildTunnelAddRow(BuildContext context) {
|
||||
var portInputFormatter = [
|
||||
FilteringTextInputFormatter.allow(RegExp(
|
||||
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$'))
|
||||
];
|
||||
|
||||
return Container(
|
||||
height: _kRowHeight,
|
||||
decoration: BoxDecoration(color: MyTheme.color(context).bg),
|
||||
child: Row(children: [
|
||||
buildTunnelInputCell(context,
|
||||
controller: localPortController,
|
||||
inputFormatters: portInputFormatter),
|
||||
const SizedBox(
|
||||
width: _kColumn1Width, child: Icon(Icons.arrow_forward_sharp)),
|
||||
buildTunnelInputCell(context,
|
||||
controller: remoteHostController, hint: 'localhost'),
|
||||
buildTunnelInputCell(context,
|
||||
controller: remotePortController,
|
||||
inputFormatters: portInputFormatter),
|
||||
SizedBox(
|
||||
width: _kColumn4Width,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
elevation: 0, side: const BorderSide(color: MyTheme.border)),
|
||||
onPressed: () async {
|
||||
int? localPort = int.tryParse(localPortController.text);
|
||||
int? remotePort = int.tryParse(remotePortController.text);
|
||||
if (localPort != null &&
|
||||
remotePort != null &&
|
||||
(remoteHostController.text.isEmpty ||
|
||||
remoteHostController.text.trim().isNotEmpty)) {
|
||||
await bind.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;
|
||||
}
|
103
flutter/lib/desktop/pages/port_forward_tab_page.dart
Normal file
103
flutter/lib/desktop/pages/port_forward_tab_page.dart
Normal 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"];
|
||||
}
|
||||
}
|
@ -5,32 +5,32 @@ import 'dart:ui' as ui;
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
|
||||
// import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../widgets/remote_menubar.dart';
|
||||
import '../../common.dart';
|
||||
import '../../mobile/widgets/dialog.dart';
|
||||
import '../../mobile/widgets/overlay.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../models/chat_model.dart';
|
||||
import '../../common/shared_state.dart';
|
||||
|
||||
final initText = '\1' * 1024;
|
||||
|
||||
class RemotePage extends StatefulWidget {
|
||||
RemotePage(
|
||||
{Key? key,
|
||||
required this.id,
|
||||
required this.tabBarHeight,
|
||||
required this.fullscreenID})
|
||||
: super(key: key);
|
||||
RemotePage({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.tabBarHeight,
|
||||
}) : super(key: key);
|
||||
|
||||
final String id;
|
||||
final double tabBarHeight;
|
||||
final Rx<String> fullscreenID;
|
||||
|
||||
@override
|
||||
_RemotePageState createState() => _RemotePageState();
|
||||
@ -41,7 +41,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
Timer? _timer;
|
||||
bool _showBar = !isWebDesktop;
|
||||
String _value = '';
|
||||
var _cursorOverImage = false.obs;
|
||||
final _cursorOverImage = false.obs;
|
||||
|
||||
final FocusNode _mobileFocusNode = FocusNode();
|
||||
final FocusNode _physicalFocusNode = FocusNode();
|
||||
@ -50,11 +50,27 @@ class _RemotePageState extends State<RemotePage>
|
||||
|
||||
late FFI _ffi;
|
||||
|
||||
void _updateTabBarHeight() {
|
||||
_ffi.canvasModel.tabBarHeight = widget.tabBarHeight;
|
||||
}
|
||||
|
||||
void _initStates(String id) {
|
||||
PrivacyModeState.init(id);
|
||||
BlockInputState.init(id);
|
||||
CurrentDisplayState.init(id);
|
||||
}
|
||||
|
||||
void _removeStates(String id) {
|
||||
PrivacyModeState.delete(id);
|
||||
BlockInputState.delete(id);
|
||||
CurrentDisplayState.delete(id);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ffi = FFI();
|
||||
_ffi.canvasModel.tabBarHeight = super.widget.tabBarHeight;
|
||||
_updateTabBarHeight();
|
||||
Get.put(_ffi, tag: widget.id);
|
||||
_ffi.connect(widget.id, tabBarHeight: super.widget.tabBarHeight);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@ -70,11 +86,12 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.listenToMouse(true);
|
||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
|
||||
// WindowManager.instance.addListener(this);
|
||||
_initStates(widget.id);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
print("REMOTE PAGE dispose ${widget.id}");
|
||||
debugPrint("REMOTE PAGE dispose ${widget.id}");
|
||||
hideMobileActionsOverlay();
|
||||
_ffi.listenToMouse(false);
|
||||
_mobileFocusNode.dispose();
|
||||
@ -90,6 +107,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
// WindowManager.instance.removeListener(this);
|
||||
Get.delete<FFI>(tag: widget.id);
|
||||
super.dispose();
|
||||
_removeStates(widget.id);
|
||||
}
|
||||
|
||||
void resetTool() {
|
||||
@ -187,19 +205,19 @@ class _RemotePageState extends State<RemotePage>
|
||||
return Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
// resizeToAvoidBottomInset: true,
|
||||
floatingActionButton: _showBar
|
||||
? null
|
||||
: FloatingActionButton(
|
||||
mini: true,
|
||||
child: Icon(Icons.expand_less),
|
||||
backgroundColor: MyTheme.accent,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showBar = !_showBar;
|
||||
});
|
||||
}),
|
||||
bottomNavigationBar:
|
||||
_showBar && hasDisplays ? getBottomAppBar(ffiModel) : null,
|
||||
// floatingActionButton: _showBar
|
||||
// ? null
|
||||
// : FloatingActionButton(
|
||||
// mini: true,
|
||||
// child: Icon(Icons.expand_less),
|
||||
// backgroundColor: MyTheme.accent,
|
||||
// onPressed: () {
|
||||
// setState(() {
|
||||
// _showBar = !_showBar;
|
||||
// });
|
||||
// }),
|
||||
// bottomNavigationBar:
|
||||
// _showBar && hasDisplays ? getBottomAppBar(ffiModel) : null,
|
||||
body: Overlay(
|
||||
initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
@ -217,6 +235,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
_updateTabBarHeight();
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(_ffi.dialogManager);
|
||||
@ -337,6 +356,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
|
||||
Widget? getBottomAppBar(FfiModel ffiModel) {
|
||||
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.basic,
|
||||
child: BottomAppBar(
|
||||
@ -371,15 +391,11 @@ class _RemotePageState extends State<RemotePage>
|
||||
: <Widget>[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(widget.fullscreenID.value.isEmpty
|
||||
icon: Icon(fullscreen.isTrue
|
||||
? Icons.fullscreen
|
||||
: Icons.close_fullscreen),
|
||||
onPressed: () {
|
||||
if (widget.fullscreenID.value.isEmpty) {
|
||||
widget.fullscreenID.value = widget.id;
|
||||
} else {
|
||||
widget.fullscreenID.value = "";
|
||||
}
|
||||
fullscreen.value = !fullscreen.value;
|
||||
},
|
||||
)
|
||||
]) +
|
||||
@ -452,7 +468,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
if (_isPhysicalMouse) {
|
||||
_ffi.handleMouse(getEvent(e, 'mousemove'),
|
||||
tabBarHeight: super.widget.tabBarHeight);
|
||||
tabBarHeight: widget.tabBarHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@ -466,7 +482,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
if (_isPhysicalMouse) {
|
||||
_ffi.handleMouse(getEvent(e, 'mousedown'),
|
||||
tabBarHeight: super.widget.tabBarHeight);
|
||||
tabBarHeight: widget.tabBarHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@ -474,7 +490,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (_isPhysicalMouse) {
|
||||
_ffi.handleMouse(getEvent(e, 'mouseup'),
|
||||
tabBarHeight: super.widget.tabBarHeight);
|
||||
tabBarHeight: widget.tabBarHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@ -482,7 +498,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (_isPhysicalMouse) {
|
||||
_ffi.handleMouse(getEvent(e, 'mousemove'),
|
||||
tabBarHeight: super.widget.tabBarHeight);
|
||||
tabBarHeight: widget.tabBarHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@ -548,6 +564,10 @@ class _RemotePageState extends State<RemotePage>
|
||||
));
|
||||
}
|
||||
paints.add(QualityMonitor(_ffi.qualityMonitorModel));
|
||||
paints.add(RemoteMenubar(
|
||||
id: widget.id,
|
||||
ffi: _ffi,
|
||||
));
|
||||
return Stack(
|
||||
children: paints,
|
||||
);
|
||||
@ -717,11 +737,11 @@ class ImagePaint extends StatelessWidget {
|
||||
width: c.getDisplayWidth() * s,
|
||||
height: c.getDisplayHeight() * s,
|
||||
child: CustomPaint(
|
||||
painter: new ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
||||
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
||||
));
|
||||
return Center(
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (_notification) {
|
||||
onNotification: (notification) {
|
||||
final percentX = _horizontal.position.extentBefore /
|
||||
(_horizontal.position.extentBefore +
|
||||
_horizontal.position.extentInside +
|
||||
@ -744,8 +764,8 @@ class ImagePaint extends StatelessWidget {
|
||||
width: c.size.width,
|
||||
height: c.size.height,
|
||||
child: CustomPaint(
|
||||
painter: new ImagePainter(
|
||||
image: m.image, x: c.x / s, y: c.y / s, scale: s),
|
||||
painter:
|
||||
ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
|
||||
));
|
||||
return _buildListener(imageWidget);
|
||||
}
|
||||
@ -799,7 +819,7 @@ class CursorPaint extends StatelessWidget {
|
||||
// final adjust = m.adjustForKeyboard();
|
||||
var s = c.scale;
|
||||
return CustomPaint(
|
||||
painter: new ImagePainter(
|
||||
painter: ImagePainter(
|
||||
image: m.image,
|
||||
x: m.x * s - m.hotx + c.x,
|
||||
y: m.y * s - m.hoty + c.y,
|
||||
@ -824,15 +844,16 @@ class ImagePainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (image == null) return;
|
||||
if (x.isNaN || y.isNaN) return;
|
||||
canvas.scale(scale, scale);
|
||||
// https://github.com/flutter/flutter/issues/76187#issuecomment-784628161
|
||||
// https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html
|
||||
var paint = new Paint();
|
||||
var paint = Paint();
|
||||
paint.filterQuality = FilterQuality.medium;
|
||||
if (scale > 10.00000) {
|
||||
paint.filterQuality = FilterQuality.high;
|
||||
}
|
||||
canvas.drawImage(image!, new Offset(x, y), paint);
|
||||
canvas.drawImage(image!, Offset(x, y), paint);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -111,7 +111,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
showMaximize: false,
|
||||
showMinimize: false,
|
||||
controller: serverModel.tabController,
|
||||
isMainWindow: true,
|
||||
tabType: DesktopTabType.cm,
|
||||
pageViewBuilder: (pageView) => Row(children: [
|
||||
Expanded(child: pageView),
|
||||
Consumer<ChatModel>(
|
||||
@ -294,7 +294,8 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
Offstage(
|
||||
offstage: client.isFileTransfer,
|
||||
child: IconButton(
|
||||
onPressed: () => gFFI.chatModel.toggleCMChatPage(client.id),
|
||||
onPressed: () => checkClickTime(
|
||||
client.id, () => gFFI.chatModel.toggleCMChatPage(client.id)),
|
||||
icon: Icon(Icons.message_outlined),
|
||||
),
|
||||
)
|
||||
@ -326,7 +327,8 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey),
|
||||
padding: EdgeInsets.all(4.0),
|
||||
child: InkWell(
|
||||
onTap: () => onTap?.call(!enabled),
|
||||
onTap: () =>
|
||||
checkClickTime(widget.client.id, () => onTap?.call(!enabled)),
|
||||
child: Image(
|
||||
image: icon,
|
||||
width: 50,
|
||||
@ -422,7 +424,8 @@ class _CmControlPanel extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.redAccent, borderRadius: BorderRadius.circular(10)),
|
||||
child: InkWell(
|
||||
onTap: () => handleDisconnect(context),
|
||||
onTap: () =>
|
||||
checkClickTime(client.id, () => handleDisconnect(context)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@ -447,7 +450,8 @@ class _CmControlPanel extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: MyTheme.accent, borderRadius: BorderRadius.circular(10)),
|
||||
child: InkWell(
|
||||
onTap: () => handleAccept(context),
|
||||
onTap: () =>
|
||||
checkClickTime(client.id, () => handleAccept(context)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@ -469,7 +473,8 @@ class _CmControlPanel extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey)),
|
||||
child: InkWell(
|
||||
onTap: () => handleDisconnect(context),
|
||||
onTap: () =>
|
||||
checkClickTime(client.id, () => handleDisconnect(context)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@ -572,3 +577,12 @@ Widget clientInfo(Client client) {
|
||||
),
|
||||
]));
|
||||
}
|
||||
|
||||
void checkClickTime(int id, Function() callback) async {
|
||||
var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
|
||||
await bind.cmCheckClickTime(connId: id);
|
||||
Timer(const Duration(milliseconds: 120), () async {
|
||||
var d = clickCallbackTime - await bind.cmGetClickTime();
|
||||
if (d > 120) callback();
|
||||
});
|
||||
}
|
||||
|
26
flutter/lib/desktop/screen/desktop_port_forward_screen.dart
Normal file
26
flutter/lib/desktop/screen/desktop_port_forward_screen.dart
Normal file
@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/port_forward_tab_page.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// multi-tab file port forward screen
|
||||
class DesktopPortForwardScreen extends StatelessWidget {
|
||||
final Map<String, dynamic> params;
|
||||
|
||||
const DesktopPortForwardScreen({Key? key, required this.params})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
||||
],
|
||||
child: Scaffold(
|
||||
body: PortForwardTabPage(
|
||||
params: params,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/connection_tab_page.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// multi-tab desktop remote screen
|
||||
@ -11,6 +12,8 @@ class DesktopRemoteScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
RxBool fullscreen = false.obs;
|
||||
Get.put(fullscreen, tag: 'fullscreen');
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
||||
|
1321
flutter/lib/desktop/widgets/material_mod_popup_menu.dart
Normal file
1321
flutter/lib/desktop/widgets/material_mod_popup_menu.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -21,18 +21,16 @@ final peerSearchTextController =
|
||||
TextEditingController(text: peerSearchText.value);
|
||||
|
||||
class _PeerWidget extends StatefulWidget {
|
||||
late final _peers;
|
||||
late final OffstageFunc _offstageFunc;
|
||||
late final PeerCardWidgetFunc _peerCardWidgetFunc;
|
||||
final Peers peers;
|
||||
final OffstageFunc offstageFunc;
|
||||
final PeerCardWidgetFunc peerCardWidgetFunc;
|
||||
|
||||
_PeerWidget(Peers peers, OffstageFunc offstageFunc,
|
||||
PeerCardWidgetFunc peerCardWidgetFunc,
|
||||
{Key? key})
|
||||
: super(key: key) {
|
||||
_peers = peers;
|
||||
_offstageFunc = offstageFunc;
|
||||
_peerCardWidgetFunc = peerCardWidgetFunc;
|
||||
}
|
||||
const _PeerWidget(
|
||||
{required this.peers,
|
||||
required this.offstageFunc,
|
||||
required this.peerCardWidgetFunc,
|
||||
Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_PeerWidgetState createState() => _PeerWidgetState();
|
||||
@ -42,9 +40,9 @@ class _PeerWidget extends StatefulWidget {
|
||||
class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
|
||||
static const int _maxQueryCount = 3;
|
||||
|
||||
var _curPeers = Set<String>();
|
||||
final _curPeers = <String>{};
|
||||
var _lastChangeTime = DateTime.now();
|
||||
var _lastQueryPeers = Set<String>();
|
||||
var _lastQueryPeers = <String>{};
|
||||
var _lastQueryTime = DateTime.now().subtract(Duration(hours: 1));
|
||||
var _queryCoun = 0;
|
||||
var _exit = false;
|
||||
@ -78,65 +76,62 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final space = 12.0;
|
||||
const space = 12.0;
|
||||
return ChangeNotifierProvider<Peers>(
|
||||
create: (context) => super.widget._peers,
|
||||
create: (context) => widget.peers,
|
||||
child: Consumer<Peers>(
|
||||
builder: (context, peers, child) => peers.peers.isEmpty
|
||||
? Center(
|
||||
child: Text(translate("Empty")),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: ObxValue<RxString>((searchText) {
|
||||
return FutureBuilder<List<Peer>>(
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final peers = snapshot.data!;
|
||||
final cards = <Widget>[];
|
||||
for (final peer in peers) {
|
||||
cards.add(Offstage(
|
||||
key: ValueKey("off${peer.id}"),
|
||||
offstage: super.widget._offstageFunc(peer),
|
||||
child: Obx(
|
||||
() => SizedBox(
|
||||
width: 220,
|
||||
height:
|
||||
peerCardUiType.value == PeerUiType.grid
|
||||
? 140
|
||||
: 42,
|
||||
child: VisibilityDetector(
|
||||
key: ValueKey(peer.id),
|
||||
onVisibilityChanged: (info) {
|
||||
final peerId =
|
||||
(info.key as ValueKey).value;
|
||||
if (info.visibleFraction > 0.00001) {
|
||||
_curPeers.add(peerId);
|
||||
} else {
|
||||
_curPeers.remove(peerId);
|
||||
}
|
||||
_lastChangeTime = DateTime.now();
|
||||
},
|
||||
child: super
|
||||
.widget
|
||||
._peerCardWidgetFunc(peer),
|
||||
),
|
||||
builder: (context, peers, child) => peers.peers.isEmpty
|
||||
? Center(
|
||||
child: Text(translate("Empty")),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: ObxValue<RxString>((searchText) {
|
||||
return FutureBuilder<List<Peer>>(
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final peers = snapshot.data!;
|
||||
final cards = <Widget>[];
|
||||
for (final peer in peers) {
|
||||
cards.add(Offstage(
|
||||
key: ValueKey("off${peer.id}"),
|
||||
offstage: widget.offstageFunc(peer),
|
||||
child: Obx(
|
||||
() => SizedBox(
|
||||
width: 220,
|
||||
height:
|
||||
peerCardUiType.value == PeerUiType.grid
|
||||
? 140
|
||||
: 42,
|
||||
child: VisibilityDetector(
|
||||
key: ValueKey(peer.id),
|
||||
onVisibilityChanged: (info) {
|
||||
final peerId =
|
||||
(info.key as ValueKey).value;
|
||||
if (info.visibleFraction > 0.00001) {
|
||||
_curPeers.add(peerId);
|
||||
} else {
|
||||
_curPeers.remove(peerId);
|
||||
}
|
||||
_lastChangeTime = DateTime.now();
|
||||
},
|
||||
child: widget.peerCardWidgetFunc(peer),
|
||||
),
|
||||
)));
|
||||
}
|
||||
return Wrap(
|
||||
spacing: space,
|
||||
runSpacing: space,
|
||||
children: cards);
|
||||
} else {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
),
|
||||
)));
|
||||
}
|
||||
},
|
||||
future: matchPeers(searchText.value, peers.peers),
|
||||
);
|
||||
}, peerSearchText),
|
||||
)),
|
||||
return Wrap(
|
||||
spacing: space, runSpacing: space, children: cards);
|
||||
} else {
|
||||
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 {
|
||||
late final _name;
|
||||
late final _loadEvent;
|
||||
late final OffstageFunc _offstageFunc;
|
||||
late final PeerCardWidgetFunc _peerCardWidgetFunc;
|
||||
late final List<Peer> _initPeers;
|
||||
final String name;
|
||||
final String loadEvent;
|
||||
final OffstageFunc offstageFunc;
|
||||
final PeerCardWidgetFunc peerCardWidgetFunc;
|
||||
final List<Peer> initPeers;
|
||||
|
||||
BasePeerWidget({Key? key}) : super(key: key) {}
|
||||
const BasePeerWidget({
|
||||
Key? key,
|
||||
required this.name,
|
||||
required this.loadEvent,
|
||||
required this.offstageFunc,
|
||||
required this.peerCardWidgetFunc,
|
||||
required this.initPeers,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _PeerWidget(Peers(_name, _loadEvent, _initPeers), _offstageFunc,
|
||||
_peerCardWidgetFunc);
|
||||
return _PeerWidget(
|
||||
peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers),
|
||||
offstageFunc: offstageFunc,
|
||||
peerCardWidgetFunc: peerCardWidgetFunc);
|
||||
}
|
||||
}
|
||||
|
||||
class RecentPeerWidget extends BasePeerWidget {
|
||||
RecentPeerWidget({Key? key}) : super(key: key) {
|
||||
super._name = "recent peer";
|
||||
super._loadEvent = "load_recent_peers";
|
||||
super._offstageFunc = (Peer _peer) => false;
|
||||
super._peerCardWidgetFunc = (Peer peer) => RecentPeerCard(
|
||||
peer: peer,
|
||||
RecentPeerWidget({Key? key})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'recent peer',
|
||||
loadEvent: 'load_recent_peers',
|
||||
offstageFunc: (Peer peer) => false,
|
||||
peerCardWidgetFunc: (Peer peer) => RecentPeerCard(
|
||||
peer: peer,
|
||||
),
|
||||
initPeers: [],
|
||||
);
|
||||
super._initPeers = [];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -210,13 +216,17 @@ class RecentPeerWidget extends BasePeerWidget {
|
||||
}
|
||||
|
||||
class FavoritePeerWidget extends BasePeerWidget {
|
||||
FavoritePeerWidget({Key? key}) : super(key: key) {
|
||||
super._name = "favorite peer";
|
||||
super._loadEvent = "load_fav_peers";
|
||||
super._offstageFunc = (Peer _peer) => false;
|
||||
super._peerCardWidgetFunc = (Peer peer) => FavoritePeerCard(peer: peer);
|
||||
super._initPeers = [];
|
||||
}
|
||||
FavoritePeerWidget({Key? key})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'favorite peer',
|
||||
loadEvent: 'load_fav_peers',
|
||||
offstageFunc: (Peer peer) => false,
|
||||
peerCardWidgetFunc: (Peer peer) => FavoritePeerCard(
|
||||
peer: peer,
|
||||
),
|
||||
initPeers: [],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -227,13 +237,17 @@ class FavoritePeerWidget extends BasePeerWidget {
|
||||
}
|
||||
|
||||
class DiscoveredPeerWidget extends BasePeerWidget {
|
||||
DiscoveredPeerWidget({Key? key}) : super(key: key) {
|
||||
super._name = "discovered peer";
|
||||
super._loadEvent = "load_lan_peers";
|
||||
super._offstageFunc = (Peer _peer) => false;
|
||||
super._peerCardWidgetFunc = (Peer peer) => DiscoveredPeerCard(peer: peer);
|
||||
super._initPeers = [];
|
||||
}
|
||||
DiscoveredPeerWidget({Key? key})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'discovered peer',
|
||||
loadEvent: 'load_lan_peers',
|
||||
offstageFunc: (Peer peer) => false,
|
||||
peerCardWidgetFunc: (Peer peer) => DiscoveredPeerCard(
|
||||
peer: peer,
|
||||
),
|
||||
initPeers: [],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -244,21 +258,26 @@ class DiscoveredPeerWidget extends BasePeerWidget {
|
||||
}
|
||||
|
||||
class AddressBookPeerWidget extends BasePeerWidget {
|
||||
AddressBookPeerWidget({Key? key}) : super(key: key) {
|
||||
super._name = "address book peer";
|
||||
super._offstageFunc =
|
||||
(Peer peer) => !_hitTag(gFFI.abModel.selectedTags, peer.tags);
|
||||
super._peerCardWidgetFunc = (Peer peer) => AddressBookPeerCard(peer: peer);
|
||||
super._initPeers = _loadPeers();
|
||||
}
|
||||
AddressBookPeerWidget({Key? key})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'address book peer',
|
||||
loadEvent: 'load_address_book_peers',
|
||||
offstageFunc: (Peer peer) =>
|
||||
!_hitTag(gFFI.abModel.selectedTags, peer.tags),
|
||||
peerCardWidgetFunc: (Peer peer) => DiscoveredPeerCard(
|
||||
peer: peer,
|
||||
),
|
||||
initPeers: _loadPeers(),
|
||||
);
|
||||
|
||||
List<Peer> _loadPeers() {
|
||||
static List<Peer> _loadPeers() {
|
||||
return gFFI.abModel.peers.map((e) {
|
||||
return Peer.fromJson(e['id'], e);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
|
||||
static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
|
||||
if (selectedTags.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
509
flutter/lib/desktop/widgets/popup_menu.dart
Normal file
509
flutter/lib/desktop/widgets/popup_menu.dart
Normal 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();
|
||||
},
|
||||
),
|
||||
)
|
||||
];
|
||||
}
|
||||
}
|
619
flutter/lib/desktop/widgets/remote_menubar.dart
Normal file
619
flutter/lib/desktop/widgets/remote_menubar.dart
Normal 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')),
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
@ -5,9 +7,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/main.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:scroll_pos/scroll_pos.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../../utils/multi_window_manager.dart';
|
||||
|
||||
@ -33,6 +36,15 @@ class TabInfo {
|
||||
required this.page});
|
||||
}
|
||||
|
||||
enum DesktopTabType {
|
||||
main,
|
||||
cm,
|
||||
remoteScreen,
|
||||
fileTransfer,
|
||||
portForward,
|
||||
rdp,
|
||||
}
|
||||
|
||||
class DesktopTabState {
|
||||
final List<TabInfo> tabs = [];
|
||||
final ScrollPosController scrollController =
|
||||
@ -63,6 +75,7 @@ class DesktopTabController {
|
||||
state.update((val) {
|
||||
val!.tabs.add(tab);
|
||||
});
|
||||
state.value.scrollController.itemCount = state.value.tabs.length;
|
||||
toIndex = state.value.tabs.length - 1;
|
||||
assert(toIndex >= 0);
|
||||
}
|
||||
@ -95,8 +108,16 @@ class DesktopTabController {
|
||||
void jumpTo(int index) {
|
||||
state.update((val) {
|
||||
val!.selected = index;
|
||||
val.pageController.jumpToPage(index);
|
||||
val.scrollController.scrollToItem(index, center: true, animate: true);
|
||||
Future.delayed(Duration.zero, (() {
|
||||
if (val.pageController.hasClients) {
|
||||
val.pageController.jumpToPage(index);
|
||||
}
|
||||
if (val.scrollController.hasClients &&
|
||||
val.scrollController.canScroll &&
|
||||
val.scrollController.itemCount >= index) {
|
||||
val.scrollController.scrollToItem(index, center: true, animate: true);
|
||||
}
|
||||
}));
|
||||
});
|
||||
onSelected?.call(index);
|
||||
}
|
||||
@ -113,11 +134,27 @@ class DesktopTabController {
|
||||
remove(state.value.selected);
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
state.value.tabs.clear();
|
||||
state.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
class TabThemeConf {
|
||||
double iconSize;
|
||||
TarBarTheme theme;
|
||||
TabThemeConf({required this.iconSize, required this.theme});
|
||||
}
|
||||
|
||||
typedef TabBuilder = Widget Function(
|
||||
String key, Widget icon, Widget label, TabThemeConf themeConf);
|
||||
typedef LabelGetter = Rx<String> Function(String key);
|
||||
|
||||
class DesktopTab extends StatelessWidget {
|
||||
final Function(String)? onTabClose;
|
||||
final TarBarTheme theme;
|
||||
final DesktopTabType tabType;
|
||||
final bool isMainWindow;
|
||||
final bool showTabBar;
|
||||
final bool showLogo;
|
||||
@ -127,23 +164,31 @@ class DesktopTab extends StatelessWidget {
|
||||
final bool showClose;
|
||||
final Widget Function(Widget pageView)? pageViewBuilder;
|
||||
final Widget? tail;
|
||||
final VoidCallback? onClose;
|
||||
final TabBuilder? tabBuilder;
|
||||
final LabelGetter? labelGetter;
|
||||
|
||||
final DesktopTabController controller;
|
||||
late final state = controller.state;
|
||||
Rx<DesktopTabState> get state => controller.state;
|
||||
|
||||
DesktopTab(
|
||||
{required this.controller,
|
||||
required this.isMainWindow,
|
||||
this.theme = const TarBarTheme.light(),
|
||||
this.onTabClose,
|
||||
this.showTabBar = true,
|
||||
this.showLogo = true,
|
||||
this.showTitle = true,
|
||||
this.showMinimize = true,
|
||||
this.showMaximize = true,
|
||||
this.showClose = true,
|
||||
this.pageViewBuilder,
|
||||
this.tail});
|
||||
const DesktopTab({
|
||||
required this.controller,
|
||||
required this.tabType,
|
||||
this.theme = const TarBarTheme.light(),
|
||||
this.onTabClose,
|
||||
this.showTabBar = true,
|
||||
this.showLogo = true,
|
||||
this.showTitle = true,
|
||||
this.showMinimize = true,
|
||||
this.showMaximize = true,
|
||||
this.showClose = true,
|
||||
this.pageViewBuilder,
|
||||
this.tail,
|
||||
this.onClose,
|
||||
this.tabBuilder,
|
||||
this.labelGetter,
|
||||
}) : isMainWindow =
|
||||
tabType == DesktopTabType.main || tabType == DesktopTabType.cm;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -172,11 +217,48 @@ class DesktopTab extends StatelessWidget {
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildBlock({required Widget child}) {
|
||||
if (tabType != DesktopTabType.main) {
|
||||
return child;
|
||||
}
|
||||
var block = false.obs;
|
||||
return Obx(() => MouseRegion(
|
||||
onEnter: (_) async {
|
||||
if (!option2bool(
|
||||
'allow-remote-config-modification',
|
||||
await bind.mainGetOption(
|
||||
key: 'allow-remote-config-modification'))) {
|
||||
var time0 = DateTime.now().millisecondsSinceEpoch;
|
||||
await bind.mainCheckMouseTime();
|
||||
Timer(const Duration(milliseconds: 120), () async {
|
||||
var d = time0 - await bind.mainGetMouseTime();
|
||||
if (d < 120) {
|
||||
block.value = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onExit: (_) => block.value = false,
|
||||
child: Stack(
|
||||
children: [
|
||||
child,
|
||||
Offstage(
|
||||
offstage: !block.value,
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
)),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildPageView() {
|
||||
return Obx(() => PageView(
|
||||
controller: state.value.pageController,
|
||||
children:
|
||||
state.value.tabs.map((tab) => tab.page).toList(growable: false)));
|
||||
return _buildBlock(
|
||||
child: Obx(() => PageView(
|
||||
controller: state.value.pageController,
|
||||
children: state.value.tabs
|
||||
.map((tab) => tab.page)
|
||||
.toList(growable: false))));
|
||||
}
|
||||
|
||||
Widget _buildBar() {
|
||||
@ -185,6 +267,11 @@ class DesktopTab extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: !Platform.isMacOS,
|
||||
child: const SizedBox(
|
||||
width: 78,
|
||||
)),
|
||||
Row(children: [
|
||||
Offstage(
|
||||
offstage: !showLogo,
|
||||
@ -217,6 +304,8 @@ class DesktopTab extends StatelessWidget {
|
||||
controller: controller,
|
||||
onTabClose: onTabClose,
|
||||
theme: theme,
|
||||
tabBuilder: tabBuilder,
|
||||
labelGetter: labelGetter,
|
||||
)),
|
||||
),
|
||||
],
|
||||
@ -229,6 +318,7 @@ class DesktopTab extends StatelessWidget {
|
||||
showMinimize: showMinimize,
|
||||
showMaximize: showMaximize,
|
||||
showClose: showClose,
|
||||
onClose: onClose,
|
||||
)
|
||||
],
|
||||
);
|
||||
@ -242,6 +332,7 @@ class WindowActionPanel extends StatelessWidget {
|
||||
final bool showMinimize;
|
||||
final bool showMaximize;
|
||||
final bool showClose;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
const WindowActionPanel(
|
||||
{Key? key,
|
||||
@ -249,7 +340,8 @@ class WindowActionPanel extends StatelessWidget {
|
||||
required this.theme,
|
||||
this.showMinimize = true,
|
||||
this.showMaximize = true,
|
||||
this.showClose = true})
|
||||
this.showClose = true,
|
||||
this.onClose})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@ -323,8 +415,12 @@ class WindowActionPanel extends StatelessWidget {
|
||||
if (mainTab) {
|
||||
windowManager.close();
|
||||
} else {
|
||||
WindowController.fromWindowId(windowId!).close();
|
||||
// only hide for multi window, not close
|
||||
Future.delayed(Duration.zero, () {
|
||||
WindowController.fromWindowId(windowId!).hide();
|
||||
});
|
||||
}
|
||||
onClose?.call();
|
||||
},
|
||||
is_close: true,
|
||||
)),
|
||||
@ -336,13 +432,20 @@ class WindowActionPanel extends StatelessWidget {
|
||||
// ignore: must_be_immutable
|
||||
class _ListView extends StatelessWidget {
|
||||
final DesktopTabController controller;
|
||||
late final Rx<DesktopTabState> state;
|
||||
final Function(String key)? onTabClose;
|
||||
final TarBarTheme theme;
|
||||
|
||||
final TabBuilder? tabBuilder;
|
||||
final LabelGetter? labelGetter;
|
||||
|
||||
Rx<DesktopTabState> get state => controller.state;
|
||||
|
||||
_ListView(
|
||||
{required this.controller, required this.onTabClose, required this.theme})
|
||||
: this.state = controller.state;
|
||||
{required this.controller,
|
||||
required this.onTabClose,
|
||||
required this.theme,
|
||||
this.tabBuilder,
|
||||
this.labelGetter});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -356,7 +459,9 @@ class _ListView extends StatelessWidget {
|
||||
final tab = e.value;
|
||||
return _Tab(
|
||||
index: index,
|
||||
label: tab.label,
|
||||
label: labelGetter == null
|
||||
? Rx<String>(tab.label)
|
||||
: labelGetter!(tab.label),
|
||||
selectedIcon: tab.selectedIcon,
|
||||
unselectedIcon: tab.unselectedIcon,
|
||||
closable: tab.closable,
|
||||
@ -364,22 +469,33 @@ class _ListView extends StatelessWidget {
|
||||
onClose: () => controller.remove(index),
|
||||
onSelected: () => controller.jumpTo(index),
|
||||
theme: theme,
|
||||
tabBuilder: tabBuilder == null
|
||||
? null
|
||||
: (Widget icon, Widget labelWidget, TabThemeConf themeConf) {
|
||||
return tabBuilder!(
|
||||
tab.label,
|
||||
icon,
|
||||
labelWidget,
|
||||
themeConf,
|
||||
);
|
||||
},
|
||||
);
|
||||
}).toList()));
|
||||
}
|
||||
}
|
||||
|
||||
class _Tab extends StatelessWidget {
|
||||
class _Tab extends StatefulWidget {
|
||||
late final int index;
|
||||
late final String label;
|
||||
late final Rx<String> label;
|
||||
late final IconData? selectedIcon;
|
||||
late final IconData? unselectedIcon;
|
||||
late final bool closable;
|
||||
late final int selected;
|
||||
late final Function() onClose;
|
||||
late final Function() onSelected;
|
||||
final RxBool _hover = false.obs;
|
||||
late final TarBarTheme theme;
|
||||
final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)?
|
||||
tabBuilder;
|
||||
|
||||
_Tab(
|
||||
{Key? key,
|
||||
@ -387,6 +503,7 @@ class _Tab extends StatelessWidget {
|
||||
required this.label,
|
||||
this.selectedIcon,
|
||||
this.unselectedIcon,
|
||||
this.tabBuilder,
|
||||
required this.closable,
|
||||
required this.selected,
|
||||
required this.onClose,
|
||||
@ -394,61 +511,87 @@ class _Tab extends StatelessWidget {
|
||||
required this.theme})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<_Tab> createState() => _TabState();
|
||||
}
|
||||
|
||||
class _TabState extends State<_Tab> with RestorationMixin {
|
||||
final RestorableBool restoreHover = RestorableBool(false);
|
||||
|
||||
Widget _buildTabContent() {
|
||||
bool showIcon =
|
||||
widget.selectedIcon != null && widget.unselectedIcon != null;
|
||||
bool isSelected = widget.index == widget.selected;
|
||||
|
||||
final icon = Offstage(
|
||||
offstage: !showIcon,
|
||||
child: Icon(
|
||||
isSelected ? widget.selectedIcon : widget.unselectedIcon,
|
||||
size: _kIconSize,
|
||||
color: isSelected
|
||||
? widget.theme.selectedtabIconColor
|
||||
: widget.theme.unSelectedtabIconColor,
|
||||
).paddingOnly(right: 5));
|
||||
final labelWidget = Obx(() {
|
||||
return Text(
|
||||
translate(widget.label.value),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: isSelected
|
||||
? widget.theme.selectedTextColor
|
||||
: widget.theme.unSelectedTextColor),
|
||||
);
|
||||
});
|
||||
|
||||
if (widget.tabBuilder == null) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
labelWidget,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return widget.tabBuilder!(icon, labelWidget,
|
||||
TabThemeConf(iconSize: _kIconSize, theme: widget.theme));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool show_icon = selectedIcon != null && unselectedIcon != null;
|
||||
bool is_selected = index == selected;
|
||||
bool show_divider = index != selected - 1 && index != selected;
|
||||
bool isSelected = widget.index == widget.selected;
|
||||
bool showDivider =
|
||||
widget.index != widget.selected - 1 && widget.index != widget.selected;
|
||||
RxBool hover = restoreHover.value.obs;
|
||||
return Ink(
|
||||
child: InkWell(
|
||||
onHover: (hover) => _hover.value = hover,
|
||||
onTap: () => onSelected(),
|
||||
onHover: (value) {
|
||||
hover.value = value;
|
||||
restoreHover.value = value;
|
||||
},
|
||||
onTap: () => widget.onSelected(),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
SizedBox(
|
||||
height: _kTabBarHeight,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: !show_icon,
|
||||
child: Icon(
|
||||
is_selected ? selectedIcon : unselectedIcon,
|
||||
size: _kIconSize,
|
||||
color: is_selected
|
||||
? theme.selectedtabIconColor
|
||||
: theme.unSelectedtabIconColor,
|
||||
).paddingOnly(right: 5)),
|
||||
Text(
|
||||
translate(label),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: is_selected
|
||||
? theme.selectedTextColor
|
||||
: theme.unSelectedTextColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
Offstage(
|
||||
offstage: !closable,
|
||||
child: Obx((() => _CloseButton(
|
||||
visiable: _hover.value,
|
||||
tabSelected: is_selected,
|
||||
onClose: () => onClose(),
|
||||
theme: theme,
|
||||
))),
|
||||
)
|
||||
_buildTabContent(),
|
||||
Obx((() => _CloseButton(
|
||||
visiable: hover.value && widget.closable,
|
||||
tabSelected: isSelected,
|
||||
onClose: () => widget.onClose(),
|
||||
theme: widget.theme,
|
||||
)))
|
||||
])).paddingSymmetric(horizontal: 10),
|
||||
Offstage(
|
||||
offstage: !show_divider,
|
||||
offstage: !showDivider,
|
||||
child: VerticalDivider(
|
||||
width: 1,
|
||||
indent: _kDividerIndent,
|
||||
endIndent: _kDividerIndent,
|
||||
color: theme.dividerColor,
|
||||
color: widget.theme.dividerColor,
|
||||
thickness: 1,
|
||||
),
|
||||
)
|
||||
@ -457,6 +600,14 @@ class _Tab extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String? get restorationId => "_Tab${widget.label.value}";
|
||||
|
||||
@override
|
||||
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||
registerForRestoration(restoreHover, 'restoreHover');
|
||||
}
|
||||
}
|
||||
|
||||
class _CloseButton extends StatelessWidget {
|
||||
@ -480,7 +631,7 @@ class _CloseButton extends StatelessWidget {
|
||||
child: Offstage(
|
||||
offstage: !visiable,
|
||||
child: InkWell(
|
||||
customBorder: RoundedRectangleBorder(),
|
||||
customBorder: const RoundedRectangleBorder(),
|
||||
onTap: () => onClose(),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/server_page.dart';
|
||||
import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
|
||||
import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart';
|
||||
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:get/get.dart';
|
||||
@ -47,6 +48,9 @@ Future<Null> main(List<String> args) async {
|
||||
case WindowType.FileTransfer:
|
||||
runFileTransferScreen(argument);
|
||||
break;
|
||||
case WindowType.PortForward:
|
||||
runPortForwardScreen(argument);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -76,14 +80,9 @@ Future<void> initEnv(String appType) async {
|
||||
}
|
||||
|
||||
void runMainApp(bool startService) async {
|
||||
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(1280, 720));
|
||||
await Future.wait([
|
||||
initEnv(kAppTypeMain),
|
||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
})
|
||||
]);
|
||||
await initEnv(kAppTypeMain);
|
||||
// trigger connection status updater
|
||||
await bind.mainCheckConnectStatus();
|
||||
if (startService) {
|
||||
// await windowManager.ensureInitialized();
|
||||
// disable tray
|
||||
@ -91,6 +90,13 @@ void runMainApp(bool startService) async {
|
||||
gFFI.serverModel.startService();
|
||||
}
|
||||
runApp(App());
|
||||
// set window option
|
||||
WindowOptions windowOptions =
|
||||
getHiddenTitleBarWindowOptions(const Size(1280, 720));
|
||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
});
|
||||
}
|
||||
|
||||
void runMobileApp() async {
|
||||
@ -133,6 +139,23 @@ void runFileTransferScreen(Map<String, dynamic> argument) async {
|
||||
);
|
||||
}
|
||||
|
||||
void runPortForwardScreen(Map<String, dynamic> argument) async {
|
||||
await initEnv(kAppTypeDesktopPortForward);
|
||||
runApp(
|
||||
GetMaterialApp(
|
||||
navigatorKey: globalKey,
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'RustDesk - Port Forward',
|
||||
theme: getCurrentTheme(),
|
||||
home: DesktopPortForwardScreen(params: argument),
|
||||
navigatorObservers: [
|
||||
// FirebaseAnalyticsObserver(analytics: analytics),
|
||||
],
|
||||
builder: _keepScaleBuilder(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void runConnectionManagerScreen() async {
|
||||
// initialize window
|
||||
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(300, 400));
|
||||
@ -182,7 +205,7 @@ class App extends StatelessWidget {
|
||||
title: 'RustDesk',
|
||||
theme: getCurrentTheme(),
|
||||
home: isDesktop
|
||||
? DesktopTabPage()
|
||||
? const DesktopTabPage()
|
||||
: !isAndroid
|
||||
? WebHomePage()
|
||||
: HomePage(),
|
||||
@ -190,8 +213,13 @@ class App extends StatelessWidget {
|
||||
// FirebaseAnalyticsObserver(analytics: analytics),
|
||||
],
|
||||
builder: isAndroid
|
||||
? (_, child) => AccessibilityListener(
|
||||
child: child,
|
||||
? (context, child) => AccessibilityListener(
|
||||
child: MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
child: child ?? Container(),
|
||||
),
|
||||
)
|
||||
: _keepScaleBuilder(),
|
||||
),
|
||||
|
@ -7,6 +7,7 @@ import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/generated_bridge.dart';
|
||||
import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
@ -17,6 +18,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import '../common.dart';
|
||||
import '../common/shared_state.dart';
|
||||
import '../mobile/widgets/dialog.dart';
|
||||
import '../mobile/widgets/overlay.dart';
|
||||
import 'peer_model.dart';
|
||||
@ -96,25 +98,26 @@ class FfiModel with ChangeNotifier {
|
||||
clearPermissions();
|
||||
}
|
||||
|
||||
void setConnectionType(bool secure, bool direct) {
|
||||
void setConnectionType(String peerId, bool secure, bool direct) {
|
||||
_secure = secure;
|
||||
_direct = direct;
|
||||
try {
|
||||
var connectionType = ConnectionTypeState.find(peerId);
|
||||
connectionType.setSecure(secure);
|
||||
connectionType.setDirect(direct);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
Image? getConnectionImage() {
|
||||
String? icon;
|
||||
if (secure == true && direct == true) {
|
||||
icon = 'secure';
|
||||
} else if (secure == false && direct == true) {
|
||||
icon = 'insecure';
|
||||
} else if (secure == false && direct == false) {
|
||||
icon = 'insecure_relay';
|
||||
} else if (secure == true && direct == false) {
|
||||
icon = 'secure_relay';
|
||||
if (secure == null || direct == null) {
|
||||
return null;
|
||||
} else {
|
||||
final icon =
|
||||
'${secure == true ? "secure" : "insecure"}${direct == true ? "" : "_relay"}';
|
||||
return Image.asset('assets/$icon.png', width: 48, height: 48);
|
||||
}
|
||||
return icon == null
|
||||
? null
|
||||
: Image.asset('assets/$icon.png', width: 48, height: 48);
|
||||
}
|
||||
|
||||
void clearPermissions() {
|
||||
@ -130,7 +133,8 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'peer_info') {
|
||||
handlePeerInfo(evt, peerId);
|
||||
} else if (name == 'connection_ready') {
|
||||
setConnectionType(evt['secure'] == 'true', evt['direct'] == 'true');
|
||||
setConnectionType(
|
||||
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
|
||||
} else if (name == 'switch_display') {
|
||||
handleSwitchDisplay(evt);
|
||||
} else if (name == 'cursor_data') {
|
||||
@ -172,9 +176,9 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'update_quality_status') {
|
||||
parent.target?.qualityMonitorModel.updateQualityStatus(evt);
|
||||
} else if (name == 'update_block_input_state') {
|
||||
updateBlockInputState(evt);
|
||||
updateBlockInputState(evt, peerId);
|
||||
} else if (name == 'update_privacy_mode') {
|
||||
updatePrivacyMode(evt);
|
||||
updatePrivacyMode(evt, peerId);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -189,7 +193,7 @@ class FfiModel with ChangeNotifier {
|
||||
handlePeerInfo(evt, peerId);
|
||||
} else if (name == 'connection_ready') {
|
||||
parent.target?.ffiModel.setConnectionType(
|
||||
evt['secure'] == 'true', evt['direct'] == 'true');
|
||||
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
|
||||
} else if (name == 'switch_display') {
|
||||
handleSwitchDisplay(evt);
|
||||
} else if (name == 'cursor_data') {
|
||||
@ -231,9 +235,9 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'update_quality_status') {
|
||||
parent.target?.qualityMonitorModel.updateQualityStatus(evt);
|
||||
} else if (name == 'update_block_input_state') {
|
||||
updateBlockInputState(evt);
|
||||
updateBlockInputState(evt, peerId);
|
||||
} else if (name == 'update_privacy_mode') {
|
||||
updatePrivacyMode(evt);
|
||||
updatePrivacyMode(evt, peerId);
|
||||
}
|
||||
};
|
||||
platformFFI.setEventCallback(cb);
|
||||
@ -297,6 +301,9 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
/// Handle the peer info event based on [evt].
|
||||
void handlePeerInfo(Map<String, dynamic> evt, String peerId) async {
|
||||
// recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
|
||||
bind.mainLoadRecentPeers();
|
||||
|
||||
parent.target?.dialogManager.dismissAll();
|
||||
_pi.version = evt['version'];
|
||||
_pi.username = evt['username'];
|
||||
@ -305,6 +312,12 @@ class FfiModel with ChangeNotifier {
|
||||
_pi.sasEnabled = evt['sas_enabled'] == "true";
|
||||
_pi.currentDisplay = int.parse(evt['current_display']);
|
||||
|
||||
try {
|
||||
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
if (isPeerAndroid) {
|
||||
_touchMode = true;
|
||||
if (parent.target?.ffiModel.permissions['keyboard'] != false) {
|
||||
@ -316,6 +329,7 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
if (evt['is_file_transfer'] == "true") {
|
||||
// TODO is file transfer
|
||||
parent.target?.fileModel.onReady();
|
||||
} else {
|
||||
_pi.displays = [];
|
||||
@ -343,13 +357,24 @@ class FfiModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
updateBlockInputState(Map<String, dynamic> evt) {
|
||||
updateBlockInputState(Map<String, dynamic> evt, String peerId) {
|
||||
_inputBlocked = evt['input_state'] == 'on';
|
||||
notifyListeners();
|
||||
try {
|
||||
BlockInputState.find(peerId).value = evt['input_state'] == 'on';
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
updatePrivacyMode(Map<String, dynamic> evt) {
|
||||
updatePrivacyMode(Map<String, dynamic> evt, String peerId) {
|
||||
notifyListeners();
|
||||
try {
|
||||
PrivacyModeState.find(peerId).value =
|
||||
bind.sessionGetToggleOptionSync(id: peerId, arg: 'privacy-mode');
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -476,39 +501,11 @@ class CanvasModel with ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720);
|
||||
final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280);
|
||||
|
||||
// Closure to perform shrink operation.
|
||||
final shrinkOp = () {
|
||||
final s = s1 < s2 ? s1 : s2;
|
||||
if (s < 1) {
|
||||
_scale = s;
|
||||
}
|
||||
};
|
||||
// Closure to perform stretch operation.
|
||||
final stretchOp = () {
|
||||
final s = s1 < s2 ? s1 : s2;
|
||||
if (s > 1) {
|
||||
_scale = s;
|
||||
}
|
||||
};
|
||||
// Closure to perform default operation(set the scale to 1.0).
|
||||
final defaultOp = () {
|
||||
_scale = 1.0;
|
||||
};
|
||||
|
||||
// // On desktop, shrink is the default behavior.
|
||||
// if (isDesktop) {
|
||||
// shrinkOp();
|
||||
// } else {
|
||||
defaultOp();
|
||||
// }
|
||||
|
||||
if (style == 'shrink') {
|
||||
shrinkOp();
|
||||
} else if (style == 'stretch') {
|
||||
stretchOp();
|
||||
_scale = 1.0;
|
||||
if (style == 'adaptive') {
|
||||
final s1 = size.width / getDisplayWidth();
|
||||
final s2 = size.height / getDisplayHeight();
|
||||
_scale = s1 < s2 ? s1 : s2;
|
||||
}
|
||||
|
||||
_x = (size.width - getDisplayWidth() * _scale) / 2;
|
||||
@ -536,11 +533,17 @@ class CanvasModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
int getDisplayWidth() {
|
||||
return parent.target?.ffiModel.display.width ?? 1080;
|
||||
final defaultWidth = (isDesktop || isWebDesktop)
|
||||
? kDesktopDefaultDisplayWidth
|
||||
: kMobileDefaultDisplayWidth;
|
||||
return parent.target?.ffiModel.display.width ?? defaultWidth;
|
||||
}
|
||||
|
||||
int getDisplayHeight() {
|
||||
return parent.target?.ffiModel.display.height ?? 720;
|
||||
final defaultHeight = (isDesktop || isWebDesktop)
|
||||
? kDesktopDefaultDisplayHeight
|
||||
: kMobileDefaultDisplayHeight;
|
||||
return parent.target?.ffiModel.display.height ?? defaultHeight;
|
||||
}
|
||||
|
||||
Size get size {
|
||||
@ -556,9 +559,19 @@ class CanvasModel with ChangeNotifier {
|
||||
var dxOffset = 0;
|
||||
var dyOffset = 0;
|
||||
if (dw > size.width) {
|
||||
final X_debugNanOrInfinite = x - dw * (x / size.width) - _x;
|
||||
if (X_debugNanOrInfinite.isInfinite || X_debugNanOrInfinite.isNaN) {
|
||||
debugPrint(
|
||||
'REMOVE ME ============================ X_debugNanOrInfinite $x,$dw,$_scale,${size.width},$_x');
|
||||
}
|
||||
dxOffset = (x - dw * (x / size.width) - _x).toInt();
|
||||
}
|
||||
if (dh > size.height) {
|
||||
final Y_debugNanOrInfinite = y - dh * (y / size.height) - _y;
|
||||
if (Y_debugNanOrInfinite.isInfinite || Y_debugNanOrInfinite.isNaN) {
|
||||
debugPrint(
|
||||
'REMOVE ME ============================ Y_debugNanOrInfinite $y,$dh,$_scale,${size.height},$_y');
|
||||
}
|
||||
dyOffset = (y - dh * (y / size.height) - _y).toInt();
|
||||
}
|
||||
_x += dxOffset;
|
||||
@ -926,16 +939,16 @@ class FFI {
|
||||
late final QualityMonitorModel qualityMonitorModel; // session
|
||||
|
||||
FFI() {
|
||||
this.imageModel = ImageModel(WeakReference(this));
|
||||
this.ffiModel = FfiModel(WeakReference(this));
|
||||
this.cursorModel = CursorModel(WeakReference(this));
|
||||
this.canvasModel = CanvasModel(WeakReference(this));
|
||||
this.serverModel = ServerModel(WeakReference(this)); // use global FFI
|
||||
this.chatModel = ChatModel(WeakReference(this));
|
||||
this.fileModel = FileModel(WeakReference(this));
|
||||
this.abModel = AbModel(WeakReference(this));
|
||||
this.userModel = UserModel(WeakReference(this));
|
||||
this.qualityMonitorModel = QualityMonitorModel(WeakReference(this));
|
||||
imageModel = ImageModel(WeakReference(this));
|
||||
ffiModel = FfiModel(WeakReference(this));
|
||||
cursorModel = CursorModel(WeakReference(this));
|
||||
canvasModel = CanvasModel(WeakReference(this));
|
||||
serverModel = ServerModel(WeakReference(this)); // use global FFI
|
||||
chatModel = ChatModel(WeakReference(this));
|
||||
fileModel = FileModel(WeakReference(this));
|
||||
abModel = AbModel(WeakReference(this));
|
||||
userModel = UserModel(WeakReference(this));
|
||||
qualityMonitorModel = QualityMonitorModel(WeakReference(this));
|
||||
}
|
||||
|
||||
/// Send a mouse tap event(down and up).
|
||||
@ -983,7 +996,7 @@ class FFI {
|
||||
// Raw Key
|
||||
void inputRawKey(int keyCode, int scanCode, bool down){
|
||||
debugPrint(scanCode.toString());
|
||||
bind.sessionInputRawKey(id: id, keycode: keyCode, scancode: scanCode, down: down);
|
||||
// bind.sessionInputRawKey(id: id, keycode: keyCode, scancode: scanCode, down: down);
|
||||
}
|
||||
|
||||
/// Send key stroke event.
|
||||
@ -1040,17 +1053,26 @@ class FFI {
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Connect with the given [id]. Only transfer file if [isFileTransfer].
|
||||
/// Connect with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward].
|
||||
void connect(String id,
|
||||
{bool isFileTransfer = false, double tabBarHeight = 0.0}) {
|
||||
if (!isFileTransfer) {
|
||||
{bool isFileTransfer = false,
|
||||
bool isPortForward = false,
|
||||
double tabBarHeight = 0.0}) {
|
||||
assert(!(isFileTransfer && isPortForward), "more than one connect type");
|
||||
if (isFileTransfer) {
|
||||
id = 'ft_${id}';
|
||||
} else if (isPortForward) {
|
||||
id = 'pf_${id}';
|
||||
} else {
|
||||
chatModel.resetClientMode();
|
||||
canvasModel.id = id;
|
||||
imageModel._id = id;
|
||||
cursorModel.id = id;
|
||||
}
|
||||
id = isFileTransfer ? 'ft_${id}' : id;
|
||||
final stream = bind.sessionConnect(id: id, isFileTransfer: isFileTransfer);
|
||||
// ignore: unused_local_variable
|
||||
final addRes = bind.sessionAddSync(
|
||||
id: id, isFileTransfer: isFileTransfer, isPortForward: isPortForward);
|
||||
final stream = bind.sessionStart(id: id);
|
||||
final cb = ffiModel.startEventListener(id);
|
||||
() async {
|
||||
await for (final message in stream) {
|
||||
@ -1092,7 +1114,7 @@ class FFI {
|
||||
ffiModel.clear();
|
||||
canvasModel.clear();
|
||||
resetModifiers();
|
||||
print("model closed");
|
||||
debugPrint("model $id closed");
|
||||
}
|
||||
|
||||
/// Send **get** command to the Rust core based on [name] and [arg].
|
||||
@ -1236,20 +1258,20 @@ class PeerInfo {
|
||||
Future<void> savePreference(String id, double xCursor, double yCursor,
|
||||
double xCanvas, double yCanvas, double scale, int currentDisplay) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final p = Map<String, dynamic>();
|
||||
final p = <String, dynamic>{};
|
||||
p['xCursor'] = xCursor;
|
||||
p['yCursor'] = yCursor;
|
||||
p['xCanvas'] = xCanvas;
|
||||
p['yCanvas'] = yCanvas;
|
||||
p['scale'] = scale;
|
||||
p['currentDisplay'] = currentDisplay;
|
||||
prefs.setString('peer' + id, json.encode(p));
|
||||
prefs.setString('peer$id', json.encode(p));
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getPreference(String id) async {
|
||||
if (!isWebDesktop) return null;
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
var p = prefs.getString('peer' + id);
|
||||
var p = prefs.getString('peer$id');
|
||||
if (p == null) return null;
|
||||
Map<String, dynamic> m = json.decode(p);
|
||||
return m;
|
||||
@ -1257,7 +1279,7 @@ Future<Map<String, dynamic>?> getPreference(String id) async {
|
||||
|
||||
void removePreference(String id) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.remove('peer' + id);
|
||||
prefs.remove('peer$id');
|
||||
}
|
||||
|
||||
void initializeCursorAndCanvas(FFI ffi) async {
|
||||
|
@ -30,7 +30,7 @@ class PlatformFFI {
|
||||
String _dir = '';
|
||||
String _homeDir = '';
|
||||
F2? _translate;
|
||||
var _eventHandlers = Map<String, Map<String, HandleEvent>>();
|
||||
final _eventHandlers = Map<String, Map<String, HandleEvent>>();
|
||||
late RustdeskImpl _ffiBind;
|
||||
late String _appType;
|
||||
void Function(Map<String, dynamic>)? _eventCallback;
|
||||
@ -50,27 +50,27 @@ class PlatformFFI {
|
||||
}
|
||||
|
||||
bool registerEventHandler(
|
||||
String event_name, String handler_name, HandleEvent handler) {
|
||||
debugPrint('registerEventHandler $event_name $handler_name');
|
||||
var handlers = _eventHandlers[event_name];
|
||||
String eventName, String handlerName, HandleEvent handler) {
|
||||
debugPrint('registerEventHandler $eventName $handlerName');
|
||||
var handlers = _eventHandlers[eventName];
|
||||
if (handlers == null) {
|
||||
_eventHandlers[event_name] = {handler_name: handler};
|
||||
_eventHandlers[eventName] = {handlerName: handler};
|
||||
return true;
|
||||
} else {
|
||||
if (handlers.containsKey(handler_name)) {
|
||||
if (handlers.containsKey(handlerName)) {
|
||||
return false;
|
||||
} else {
|
||||
handlers[handler_name] = handler;
|
||||
handlers[handlerName] = handler;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void unregisterEventHandler(String event_name, String handler_name) {
|
||||
debugPrint('unregisterEventHandler $event_name $handler_name');
|
||||
var handlers = _eventHandlers[event_name];
|
||||
void unregisterEventHandler(String eventName, String handlerName) {
|
||||
debugPrint('unregisterEventHandler $eventName $handlerName');
|
||||
var handlers = _eventHandlers[eventName];
|
||||
if (handlers != null) {
|
||||
handlers.remove(handler_name);
|
||||
handlers.remove(handlerName);
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,7 +117,7 @@ class PlatformFFI {
|
||||
_homeDir = (await getDownloadsDirectory())?.path ?? "";
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
print("initialize failed: $e");
|
||||
}
|
||||
String id = 'NA';
|
||||
String name = 'Flutter';
|
||||
@ -151,7 +151,7 @@ class PlatformFFI {
|
||||
await _ffiBind.mainSetHomeDir(home: _homeDir);
|
||||
await _ffiBind.mainInit(appDir: _dir);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
print("initialize failed: $e");
|
||||
}
|
||||
version = await getVersion();
|
||||
}
|
||||
|
@ -10,9 +10,8 @@ class Peer {
|
||||
final List<dynamic> tags;
|
||||
bool online = false;
|
||||
|
||||
Peer.fromJson(String id, Map<String, dynamic> json)
|
||||
: id = id,
|
||||
username = json['username'] ?? '',
|
||||
Peer.fromJson(this.id, Map<String, dynamic> json)
|
||||
: username = json['username'] ?? '',
|
||||
hostname = json['hostname'] ?? '',
|
||||
platform = json['platform'] ?? '',
|
||||
tags = json['tags'] ?? [];
|
||||
@ -35,57 +34,52 @@ class Peer {
|
||||
}
|
||||
|
||||
class Peers extends ChangeNotifier {
|
||||
late String _name;
|
||||
late List<Peer> _peers;
|
||||
late final _loadEvent;
|
||||
final String name;
|
||||
final String loadEvent;
|
||||
List<Peer> peers;
|
||||
static const _cbQueryOnlines = 'callback_query_onlines';
|
||||
|
||||
Peers(String name, String loadEvent, List<Peer> _initPeers) {
|
||||
_name = name;
|
||||
_loadEvent = loadEvent;
|
||||
_peers = _initPeers;
|
||||
platformFFI.registerEventHandler(_cbQueryOnlines, _name, (evt) {
|
||||
Peers({required this.name, required this.peers, required this.loadEvent}) {
|
||||
platformFFI.registerEventHandler(_cbQueryOnlines, name, (evt) {
|
||||
_updateOnlineState(evt);
|
||||
});
|
||||
platformFFI.registerEventHandler(_loadEvent, _name, (evt) {
|
||||
platformFFI.registerEventHandler(loadEvent, name, (evt) {
|
||||
_updatePeers(evt);
|
||||
});
|
||||
}
|
||||
|
||||
List<Peer> get peers => _peers;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
platformFFI.unregisterEventHandler(_cbQueryOnlines, _name);
|
||||
platformFFI.unregisterEventHandler(_loadEvent, _name);
|
||||
platformFFI.unregisterEventHandler(_cbQueryOnlines, name);
|
||||
platformFFI.unregisterEventHandler(loadEvent, name);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Peer getByIndex(int index) {
|
||||
if (index < _peers.length) {
|
||||
return _peers[index];
|
||||
if (index < peers.length) {
|
||||
return peers[index];
|
||||
} else {
|
||||
return Peer.loading();
|
||||
}
|
||||
}
|
||||
|
||||
int getPeersCount() {
|
||||
return _peers.length;
|
||||
return peers.length;
|
||||
}
|
||||
|
||||
void _updateOnlineState(Map<String, dynamic> evt) {
|
||||
evt['onlines'].split(',').forEach((online) {
|
||||
for (var i = 0; i < _peers.length; i++) {
|
||||
if (_peers[i].id == online) {
|
||||
_peers[i].online = true;
|
||||
for (var i = 0; i < peers.length; i++) {
|
||||
if (peers[i].id == online) {
|
||||
peers[i].online = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
evt['offlines'].split(',').forEach((offline) {
|
||||
for (var i = 0; i < _peers.length; i++) {
|
||||
if (_peers[i].id == offline) {
|
||||
_peers[i].online = false;
|
||||
for (var i = 0; i < peers.length; i++) {
|
||||
if (peers[i].id == offline) {
|
||||
peers[i].online = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -95,19 +89,19 @@ class Peers extends ChangeNotifier {
|
||||
|
||||
void _updatePeers(Map<String, dynamic> evt) {
|
||||
final onlineStates = _getOnlineStates();
|
||||
_peers = _decodePeers(evt['peers']);
|
||||
_peers.forEach((peer) {
|
||||
peers = _decodePeers(evt['peers']);
|
||||
for (var peer in peers) {
|
||||
final state = onlineStates[peer.id];
|
||||
peer.online = state != null && state != false;
|
||||
});
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Map<String, bool> _getOnlineStates() {
|
||||
var onlineStates = new Map<String, bool>();
|
||||
_peers.forEach((peer) {
|
||||
var onlineStates = <String, bool>{};
|
||||
for (var peer in peers) {
|
||||
onlineStates[peer.id] = peer.online;
|
||||
});
|
||||
}
|
||||
return onlineStates;
|
||||
}
|
||||
|
||||
@ -121,7 +115,7 @@ class Peers extends ChangeNotifier {
|
||||
Peer.fromJson(s[0] as String, s[1] as Map<String, dynamic>))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
print('peers(): $e');
|
||||
debugPrint('peers(): $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -35,10 +34,11 @@ class RustDeskMultiWindowManager {
|
||||
|
||||
int? _remoteDesktopWindowId;
|
||||
int? _fileTransferWindowId;
|
||||
int? _portForwardWindowId;
|
||||
|
||||
Future<dynamic> new_remote_desktop(String remote_id) async {
|
||||
Future<dynamic> newRemoteDesktop(String remoteId) async {
|
||||
final msg =
|
||||
jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remote_id});
|
||||
jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remoteId});
|
||||
|
||||
try {
|
||||
final ids = await DesktopMultiWindow.getAllSubWindowIds();
|
||||
@ -62,9 +62,9 @@ class RustDeskMultiWindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> new_file_transfer(String remote_id) async {
|
||||
Future<dynamic> newFileTransfer(String remoteId) async {
|
||||
final msg =
|
||||
jsonEncode({"type": WindowType.FileTransfer.index, "id": remote_id});
|
||||
jsonEncode({"type": WindowType.FileTransfer.index, "id": remoteId});
|
||||
|
||||
try {
|
||||
final ids = await DesktopMultiWindow.getAllSubWindowIds();
|
||||
@ -87,6 +87,31 @@ class RustDeskMultiWindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> newPortForward(String remoteId, bool isRDP) async {
|
||||
final msg = jsonEncode(
|
||||
{"type": WindowType.PortForward.index, "id": remoteId, "isRDP": isRDP});
|
||||
|
||||
try {
|
||||
final ids = await DesktopMultiWindow.getAllSubWindowIds();
|
||||
if (!ids.contains(_portForwardWindowId)) {
|
||||
_portForwardWindowId = null;
|
||||
}
|
||||
} on Error {
|
||||
_portForwardWindowId = null;
|
||||
}
|
||||
if (_portForwardWindowId == null) {
|
||||
final portForwardController = await DesktopMultiWindow.createWindow(msg);
|
||||
portForwardController
|
||||
..setFrame(const Offset(0, 0) & const Size(1280, 720))
|
||||
..center()
|
||||
..setTitle("rustdesk - port forward")
|
||||
..show();
|
||||
_portForwardWindowId = portForwardController.windowId;
|
||||
} else {
|
||||
return call(WindowType.PortForward, "new_port_forward", msg);
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> call(WindowType type, String methodName, dynamic args) async {
|
||||
int? windowId = findWindowByType(type);
|
||||
if (windowId == null) {
|
||||
@ -104,7 +129,7 @@ class RustDeskMultiWindowManager {
|
||||
case WindowType.FileTransfer:
|
||||
return _fileTransferWindowId;
|
||||
case WindowType.PortForward:
|
||||
break;
|
||||
return _portForwardWindowId;
|
||||
case WindowType.Unknown:
|
||||
break;
|
||||
}
|
||||
@ -120,7 +145,7 @@ class RustDeskMultiWindowManager {
|
||||
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) {
|
||||
// skip main window, use window manager instead
|
||||
return;
|
||||
|
@ -235,12 +235,10 @@ packages:
|
||||
dash_chat_2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: feat_maxWidth
|
||||
resolved-ref: "3946ecf86d3600b54632fd80d0eb0ef0e74f2d6a"
|
||||
url: "https://github.com/fufesou/Dash-Chat-2"
|
||||
source: git
|
||||
version: "0.0.12"
|
||||
name: dash_chat_2
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.0.14"
|
||||
desktop_drop:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -252,8 +250,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: e013c81d75320bbf28adddeaadf462264ee6039d
|
||||
resolved-ref: e013c81d75320bbf28adddeaadf462264ee6039d
|
||||
ref: e0368a023ba195462acc00d33ab361b499f0e413
|
||||
resolved-ref: e0368a023ba195462acc00d33ab361b499f0e413
|
||||
url: "https://github.com/Kingtous/rustdesk_desktop_multi_window"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
@ -849,7 +847,7 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: rxdart
|
||||
url: "https://pub.flutter-io.cn"
|
||||
@ -1244,8 +1242,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: a25f1776ccc1119cbb2a8541174293aa36d532ed
|
||||
resolved-ref: a25f1776ccc1119cbb2a8541174293aa36d532ed
|
||||
ref: "799ef079e87938c3f4340591b4330c2598f38bb9"
|
||||
resolved-ref: "799ef079e87938c3f4340591b4330c2598f38bb9"
|
||||
url: "https://github.com/Kingtous/rustdesk_window_manager"
|
||||
source: git
|
||||
version: "0.2.6"
|
||||
|
@ -34,16 +34,13 @@ dependencies:
|
||||
provider: ^6.0.3
|
||||
tuple: ^2.0.0
|
||||
wakelock: ^0.5.2
|
||||
device_info_plus: ^4.0.2
|
||||
device_info_plus: ^4.1.2
|
||||
firebase_analytics: ^9.1.5
|
||||
package_info_plus: ^1.4.2
|
||||
url_launcher: ^6.0.9
|
||||
shared_preferences: ^2.0.6
|
||||
toggle_switch: ^1.4.0
|
||||
dash_chat_2:
|
||||
git:
|
||||
url: https://github.com/fufesou/Dash-Chat-2
|
||||
ref: feat_maxWidth
|
||||
dash_chat_2: ^0.0.14
|
||||
draggable_float_widget: ^0.0.2
|
||||
settings_ui: ^2.0.2
|
||||
flutter_breadcrumb: ^1.0.1
|
||||
@ -61,11 +58,11 @@ dependencies:
|
||||
window_manager:
|
||||
git:
|
||||
url: https://github.com/Kingtous/rustdesk_window_manager
|
||||
ref: a25f1776ccc1119cbb2a8541174293aa36d532ed
|
||||
ref: 799ef079e87938c3f4340591b4330c2598f38bb9
|
||||
desktop_multi_window:
|
||||
git:
|
||||
url: https://github.com/Kingtous/rustdesk_desktop_multi_window
|
||||
ref: e013c81d75320bbf28adddeaadf462264ee6039d
|
||||
ref: e0368a023ba195462acc00d33ab361b499f0e413
|
||||
freezed_annotation: ^2.0.3
|
||||
tray_manager:
|
||||
git:
|
||||
@ -76,6 +73,7 @@ dependencies:
|
||||
contextmenu: ^3.0.0
|
||||
desktop_drop: ^0.3.3
|
||||
scroll_pos: ^0.3.0
|
||||
rxdart: ^0.27.5
|
||||
|
||||
dev_dependencies:
|
||||
flutter_launcher_icons: ^0.9.1
|
||||
|
@ -5,25 +5,36 @@ import os
|
||||
import glob
|
||||
from tabnanny import check
|
||||
|
||||
def pad_start(s, n, c = ' '):
|
||||
if len(s) >= n:
|
||||
return s
|
||||
return c * (n - len(s)) + s
|
||||
|
||||
def safe_unicode(s):
|
||||
res = ""
|
||||
for c in s:
|
||||
res += r"\u{}".format(pad_start(hex(ord(c))[2:], 4, '0'))
|
||||
return res
|
||||
|
||||
def main():
|
||||
print('export const LANGS = {')
|
||||
for fn in glob.glob('../../../src/lang/*'):
|
||||
lang = os.path.basename(fn)[:-3]
|
||||
if lang == 'template': continue
|
||||
print(' %s: {'%lang)
|
||||
for ln in open(fn):
|
||||
for ln in open(fn, encoding='utf-8'):
|
||||
ln = ln.strip()
|
||||
if ln.startswith('("'):
|
||||
toks = ln.split('", "')
|
||||
assert(len(toks) == 2)
|
||||
a = toks[0][2:]
|
||||
b = toks[1][:-3]
|
||||
print(' "%s": "%s",'%(a, b))
|
||||
print(' "%s": "%s",'%(safe_unicode(a), safe_unicode(b)))
|
||||
print(' },')
|
||||
print('}')
|
||||
check_if_retry = ['', False]
|
||||
KEY_MAP = ['', False]
|
||||
for ln in open('../../../src/client.rs'):
|
||||
for ln in open('../../../src/client.rs', encoding='utf-8'):
|
||||
ln = ln.strip()
|
||||
if 'check_if_retry' in ln:
|
||||
check_if_retry[1] = True
|
||||
@ -55,7 +66,7 @@ def main():
|
||||
print('export const KEY_MAP: any = {')
|
||||
print(KEY_MAP[0])
|
||||
print('}')
|
||||
for ln in open('../../../Cargo.toml'):
|
||||
for ln in open('../../../Cargo.toml', encoding='utf-8'):
|
||||
if ln.startswith('version ='):
|
||||
print('export const ' + ln)
|
||||
|
||||
|
@ -2,7 +2,7 @@ use std::{
|
||||
collections::HashMap,
|
||||
net::SocketAddr,
|
||||
ops::{Deref, Not},
|
||||
sync::{mpsc, Arc, Mutex, RwLock},
|
||||
sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock},
|
||||
};
|
||||
|
||||
pub use async_trait::async_trait;
|
||||
@ -37,7 +37,6 @@ use hbb_common::{
|
||||
};
|
||||
pub use helper::LatencyController;
|
||||
pub use helper::*;
|
||||
use scrap::Image;
|
||||
use scrap::{
|
||||
codec::{Decoder, DecoderCfg},
|
||||
VpxDecoderConfig, VpxVideoCodecId,
|
||||
@ -47,7 +46,12 @@ pub use super::lang::*;
|
||||
|
||||
pub mod file_trait;
|
||||
pub mod helper;
|
||||
pub mod io_loop;
|
||||
|
||||
pub static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||
pub static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||
pub static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||
pub const MILLI1: Duration = Duration::from_millis(1);
|
||||
pub const SEC30: Duration = Duration::from_secs(30);
|
||||
|
||||
/// Client of the remote desktop.
|
||||
@ -55,7 +59,23 @@ pub struct Client;
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref AUDIO_HOST: Host = cpal::default_host();
|
||||
static ref AUDIO_HOST: Host = cpal::default_host();
|
||||
}
|
||||
use rdev::{Event, EventType::*, Key as RdevKey, Keyboard as RdevKeyboard, KeyboardState};
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref ENIGO: Arc<Mutex<enigo::Enigo>> = Arc::new(Mutex::new(enigo::Enigo::new()));
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn get_key_state(key: enigo::Key) -> bool {
|
||||
use enigo::KeyboardControllable;
|
||||
#[cfg(target_os = "macos")]
|
||||
if key == enigo::Key::NumLock {
|
||||
return true;
|
||||
}
|
||||
ENIGO.lock().unwrap().get_key_state(key)
|
||||
}
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
@ -846,8 +866,7 @@ impl VideoHandler {
|
||||
#[derive(Default)]
|
||||
pub struct LoginConfigHandler {
|
||||
id: String,
|
||||
pub is_file_transfer: bool,
|
||||
is_port_forward: bool,
|
||||
pub conn_type: ConnType,
|
||||
hash: Hash,
|
||||
password: Vec<u8>, // remember password for reconnect
|
||||
pub remember: bool,
|
||||
@ -886,12 +905,10 @@ impl LoginConfigHandler {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `id` - id of peer
|
||||
/// * `is_file_transfer` - Whether the connection is file transfer.
|
||||
/// * `is_port_forward` - Whether the connection is port forward.
|
||||
pub fn initialize(&mut self, id: String, is_file_transfer: bool, is_port_forward: bool) {
|
||||
/// * `conn_type` - Connection type enum.
|
||||
pub fn initialize(&mut self, id: String, conn_type: ConnType) {
|
||||
self.id = id;
|
||||
self.is_file_transfer = is_file_transfer;
|
||||
self.is_port_forward = is_port_forward;
|
||||
self.conn_type = conn_type;
|
||||
let config = self.load_config();
|
||||
self.remember = !config.password.is_empty();
|
||||
self.config = config;
|
||||
@ -1048,7 +1065,8 @@ impl LoginConfigHandler {
|
||||
///
|
||||
/// * `ignore_default` - If `true`, ignore the default value of the option.
|
||||
fn get_option_message(&self, ignore_default: bool) -> Option<OptionMessage> {
|
||||
if self.is_port_forward || self.is_file_transfer {
|
||||
if self.conn_type.eq(&ConnType::FILE_TRANSFER) || self.conn_type.eq(&ConnType::PORT_FORWARD)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut n = 0;
|
||||
@ -1094,7 +1112,8 @@ impl LoginConfigHandler {
|
||||
}
|
||||
|
||||
pub fn get_option_message_after_login(&self) -> Option<OptionMessage> {
|
||||
if self.is_port_forward || self.is_file_transfer {
|
||||
if self.conn_type.eq(&ConnType::FILE_TRANSFER) || self.conn_type.eq(&ConnType::PORT_FORWARD)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut n = 0;
|
||||
@ -1260,13 +1279,13 @@ impl LoginConfigHandler {
|
||||
///
|
||||
/// * `username` - The name of the peer.
|
||||
/// * `pi` - The peer info.
|
||||
pub fn handle_peer_info(&mut self, username: String, pi: PeerInfo) {
|
||||
pub fn handle_peer_info(&mut self, pi: &PeerInfo) {
|
||||
if !pi.version.is_empty() {
|
||||
self.version = hbb_common::get_version_number(&pi.version);
|
||||
}
|
||||
self.features = pi.features.into_option();
|
||||
self.features = pi.features.clone().into_option();
|
||||
let serde = PeerInfoSerde {
|
||||
username,
|
||||
username: pi.username.clone(),
|
||||
hostname: pi.hostname.clone(),
|
||||
platform: pi.platform.clone(),
|
||||
};
|
||||
@ -1330,19 +1349,20 @@ impl LoginConfigHandler {
|
||||
version: crate::VERSION.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
if self.is_file_transfer {
|
||||
lr.set_file_transfer(FileTransfer {
|
||||
match self.conn_type {
|
||||
ConnType::FILE_TRANSFER => lr.set_file_transfer(FileTransfer {
|
||||
dir: self.get_remote_dir(),
|
||||
show_hidden: !self.get_option("remote_show_hidden").is_empty(),
|
||||
..Default::default()
|
||||
});
|
||||
} else if self.is_port_forward {
|
||||
lr.set_port_forward(PortForward {
|
||||
}),
|
||||
ConnType::PORT_FORWARD => lr.set_port_forward(PortForward {
|
||||
host: self.port_forward.0.clone(),
|
||||
port: self.port_forward.1,
|
||||
..Default::default()
|
||||
});
|
||||
}),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_login_request(lr);
|
||||
msg_out
|
||||
@ -1651,6 +1671,12 @@ pub trait Interface: Send + Clone + 'static + Sized {
|
||||
fn handle_login_error(&mut self, err: &str) -> bool;
|
||||
fn handle_peer_info(&mut self, pi: PeerInfo);
|
||||
fn set_force_relay(&mut self, direct: bool, received: bool);
|
||||
fn is_file_transfer(&self) -> bool;
|
||||
fn is_port_forward(&self) -> bool;
|
||||
fn is_rdp(&self) -> bool;
|
||||
fn on_error(&self, err: &str) {
|
||||
self.msgbox("error", "Error", err);
|
||||
}
|
||||
fn is_force_relay(&self) -> bool;
|
||||
async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream);
|
||||
async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream);
|
||||
|
@ -1,4 +1,4 @@
|
||||
use hbb_common::{fs, message_proto::*};
|
||||
use hbb_common::{fs, message_proto::*, log};
|
||||
|
||||
use super::{Data, Interface};
|
||||
|
||||
@ -114,4 +114,26 @@ pub trait FileManager: Interface {
|
||||
fn resume_job(&self, id: i32, is_remote: bool) {
|
||||
self.send(Data::ResumeJob((id, is_remote)));
|
||||
}
|
||||
|
||||
fn set_confirm_override_file(
|
||||
&self,
|
||||
id: i32,
|
||||
file_num: i32,
|
||||
need_override: bool,
|
||||
remember: bool,
|
||||
is_upload: bool,
|
||||
) {
|
||||
log::info!(
|
||||
"confirm file transfer, job: {}, need_override: {}",
|
||||
id,
|
||||
need_override
|
||||
);
|
||||
self.send(Data::SetConfirmOverrideFile((
|
||||
id,
|
||||
file_num,
|
||||
need_override,
|
||||
remember,
|
||||
is_upload,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
1208
src/client/io_loop.rs
Normal file
1208
src/client/io_loop.rs
Normal file
File diff suppressed because it is too large
Load Diff
1875
src/flutter.rs
1875
src/flutter.rs
File diff suppressed because it is too large
Load Diff
@ -13,21 +13,22 @@ use hbb_common::{
|
||||
};
|
||||
use hbb_common::{password_security, ResultType};
|
||||
|
||||
use crate::client::file_trait::FileManager;
|
||||
use crate::{client::file_trait::FileManager, flutter::{session_add, session_start_}};
|
||||
use crate::common::make_fd_to_json;
|
||||
use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state};
|
||||
use crate::flutter::{self, Session, SESSIONS};
|
||||
use crate::flutter::{self, SESSIONS};
|
||||
use crate::start_server;
|
||||
use crate::ui_interface;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::ui_interface::{change_id, check_connect_status, is_ok_change_id};
|
||||
use crate::ui_interface::{
|
||||
check_super_user_permission, discover, forget_password, get_api_server, get_app_name,
|
||||
get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_langs,
|
||||
get_license, get_local_option, get_option, get_options, get_peer, get_peer_option, get_socks,
|
||||
get_sound_inputs, get_uuid, get_version, has_hwcodec, has_rendezvous_service, post_request,
|
||||
set_local_option, set_option, set_options, set_peer_option, set_permanent_password, set_socks,
|
||||
store_fav, test_if_valid_server, update_temporary_password, using_public_server,
|
||||
check_mouse_time, check_super_user_permission, discover, forget_password, get_api_server,
|
||||
get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers,
|
||||
get_langs, get_license, get_local_option, get_mouse_time, get_option, get_options, get_peer,
|
||||
get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, has_hwcodec,
|
||||
has_rendezvous_service, post_request, set_local_option, set_option, set_options,
|
||||
set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server,
|
||||
update_temporary_password, using_public_server,
|
||||
};
|
||||
|
||||
fn initialize(app_dir: &str) {
|
||||
@ -107,13 +108,18 @@ pub fn host_stop_system_key_propagate(stopped: bool) {
|
||||
crate::platform::windows::stop_system_key_propagate(stopped);
|
||||
}
|
||||
|
||||
pub fn session_connect(
|
||||
events2ui: StreamSink<EventToUI>,
|
||||
id: String,
|
||||
is_file_transfer: bool,
|
||||
) -> ResultType<()> {
|
||||
Session::start(&id, is_file_transfer, events2ui);
|
||||
Ok(())
|
||||
// FIXME: -> ResultType<()> cannot be parsed by frb_codegen
|
||||
// thread 'main' panicked at 'Failed to parse function output type `ResultType<()>`', $HOME\.cargo\git\checkouts\flutter_rust_bridge-ddba876d3ebb2a1e\e5adce5\frb_codegen\src\parser\mod.rs:151:25
|
||||
pub fn session_add_sync(id: String, is_file_transfer: bool, is_port_forward: bool) -> SyncReturn<String> {
|
||||
if let Err(e) = session_add(&id, is_file_transfer, is_port_forward) {
|
||||
SyncReturn(format!("Failed to add session with id {}, {}", &id, e))
|
||||
} else {
|
||||
SyncReturn("".to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_start(events2ui: StreamSink<EventToUI>, id: String) -> ResultType<()> {
|
||||
session_start_(&id, events2ui)
|
||||
}
|
||||
|
||||
pub fn session_get_remember(id: String) -> Option<bool> {
|
||||
@ -126,7 +132,7 @@ pub fn session_get_remember(id: String) -> Option<bool> {
|
||||
|
||||
pub fn session_get_toggle_option(id: String, arg: String) -> Option<bool> {
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
Some(session.get_toggle_option(&arg))
|
||||
Some(session.get_toggle_option(arg))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -137,17 +143,9 @@ pub fn session_get_toggle_option_sync(id: String, arg: String) -> SyncReturn<boo
|
||||
SyncReturn(res)
|
||||
}
|
||||
|
||||
pub fn session_get_image_quality(id: String) -> Option<String> {
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
Some(session.get_image_quality())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_get_option(id: String, arg: String) -> Option<String> {
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
Some(session.get_option(&arg))
|
||||
Some(session.get_option(arg))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -155,7 +153,7 @@ pub fn session_get_option(id: String, arg: String) -> Option<String> {
|
||||
|
||||
pub fn session_login(id: String, password: String, remember: bool) {
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
session.login(&password, remember);
|
||||
session.login(password, remember);
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,7 +166,7 @@ pub fn session_close(id: String) {
|
||||
|
||||
pub fn session_refresh(id: String) {
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
session.refresh();
|
||||
session.refresh_video();
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,14 +177,36 @@ pub fn session_reconnect(id: String) {
|
||||
}
|
||||
|
||||
pub fn session_toggle_option(id: String, value: String) {
|
||||
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
|
||||
session.toggle_option(value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_get_image_quality(id: String) -> Option<String> {
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
session.toggle_option(&value);
|
||||
Some(session.get_image_quality())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_set_image_quality(id: String, value: String) {
|
||||
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
|
||||
session.save_image_quality(value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_get_custom_image_quality(id: String) -> Option<Vec<i32>> {
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
session.set_image_quality(&value);
|
||||
Some(session.get_custom_image_quality())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_set_custom_image_quality(id: String, value: i32) {
|
||||
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
|
||||
session.save_custom_image_quality(value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,11 +228,11 @@ pub fn session_switch_display(id: String, value: i32) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_input_raw_key(id: String, keycode: i32, scancode:i32, down: bool){
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
session.input_raw_key(keycode, scancode, down);
|
||||
}
|
||||
}
|
||||
// pub fn session_input_raw_key(id: String, keycode: i32, scancode:i32, down: bool){
|
||||
// if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
// session.input_raw_key(keycode, scancode, down);
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn session_input_key(
|
||||
id: String,
|
||||
@ -250,7 +270,7 @@ pub fn session_peer_option(id: String, name: String, value: String) {
|
||||
|
||||
pub fn session_get_peer_option(id: String, name: String) -> String {
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
return session.get_option(&name);
|
||||
return session.get_option(name);
|
||||
}
|
||||
"".to_string()
|
||||
}
|
||||
@ -349,7 +369,7 @@ pub fn session_get_platform(id: String, is_remote: bool) -> String {
|
||||
|
||||
pub fn session_load_last_transfer_jobs(id: String) {
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
return session.load_last_jobs();
|
||||
// return session.load_last_jobs();
|
||||
} else {
|
||||
// a tip for flutter dev
|
||||
eprintln!(
|
||||
@ -473,7 +493,7 @@ pub fn main_get_connect_status() -> String {
|
||||
|
||||
pub fn main_check_connect_status() {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
check_connect_status(true);
|
||||
check_mouse_time(); // avoid multi calls
|
||||
}
|
||||
|
||||
pub fn main_is_using_public_server() -> bool {
|
||||
@ -598,12 +618,32 @@ pub fn main_load_lan_peers() {
|
||||
{
|
||||
let data = HashMap::from([
|
||||
("name", "load_lan_peers".to_owned()),
|
||||
("peers", serde_json::to_string(&get_lan_peers()).unwrap_or_default()),
|
||||
(
|
||||
"peers",
|
||||
serde_json::to_string(&get_lan_peers()).unwrap_or_default(),
|
||||
),
|
||||
]);
|
||||
s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned()));
|
||||
};
|
||||
}
|
||||
|
||||
pub fn session_add_port_forward(
|
||||
id: String,
|
||||
local_port: i32,
|
||||
remote_host: String,
|
||||
remote_port: i32,
|
||||
) {
|
||||
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
|
||||
session.add_port_forward(local_port, remote_host, remote_port);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_remove_port_forward(id: String, local_port: i32) {
|
||||
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
|
||||
session.remove_port_forward(local_port);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main_get_last_remote_id() -> String {
|
||||
// if !config::APP_DIR.read().unwrap().is_empty() {
|
||||
// res = LocalConfig::get_remote_id();
|
||||
@ -667,7 +707,6 @@ pub fn main_has_hwcodec() -> bool {
|
||||
has_hwcodec()
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn session_send_mouse(id: String, msg: String) {
|
||||
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(&msg) {
|
||||
let alt = m.get("alt").is_some();
|
||||
@ -745,6 +784,14 @@ pub fn main_check_super_user_permission() -> bool {
|
||||
check_super_user_permission()
|
||||
}
|
||||
|
||||
pub fn main_check_mouse_time() {
|
||||
check_mouse_time();
|
||||
}
|
||||
|
||||
pub fn main_get_mouse_time() -> f64 {
|
||||
get_mouse_time()
|
||||
}
|
||||
|
||||
pub fn cm_send_chat(conn_id: i32, msg: String) {
|
||||
connection_manager::send_chat(conn_id, msg);
|
||||
}
|
||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", "正在重启远程设备"),
|
||||
("remote_restarting_tip", "远程设备正在重启, 请关闭当前提示框, 并在一段时间后使用永久密码重新连接"),
|
||||
("Copied", "已复制"),
|
||||
("Exit Fullscreen", "退出全屏"),
|
||||
("Fullscreen", "全屏"),
|
||||
("Mobile Actions", "移动端操作"),
|
||||
("Select Monitor", "选择监视器"),
|
||||
("Control Actions", "控制操作"),
|
||||
("Display Settings", "显示设置"),
|
||||
("Ratio", "比例"),
|
||||
("Image Quality", "画质"),
|
||||
("Scroll Style", "滚屏方式"),
|
||||
("Show Menubar", "显示菜单栏"),
|
||||
("Hide Menubar", "隐藏菜单栏"),
|
||||
("Direct Connection", "直接连接"),
|
||||
("Relay Connection", "中继连接"),
|
||||
("Secure Connection", "安全连接"),
|
||||
("Insecure Connection", "非安全连接"),
|
||||
("Scale original", "原始尺寸"),
|
||||
("Scale adaptive", "适应窗口"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Ukončete celou obrazovku"),
|
||||
("Fullscreen", "Celá obrazovka"),
|
||||
("Mobile Actions", "Mobilní akce"),
|
||||
("Select Monitor", "Vyberte možnost Monitor"),
|
||||
("Control Actions", "Ovládací akce"),
|
||||
("Display Settings", "Nastavení obrazovky"),
|
||||
("Ratio", "Poměr"),
|
||||
("Image Quality", "Kvalita obrazu"),
|
||||
("Scroll Style", "Štýl posúvania"),
|
||||
("Show Menubar", "Zobrazit panel nabídek"),
|
||||
("Hide Menubar", "skrýt panel nabídek"),
|
||||
("Direct Connection", "Přímé spojení"),
|
||||
("Relay Connection", "Připojení relé"),
|
||||
("Secure Connection", "Zabezpečené připojení"),
|
||||
("Insecure Connection", "Nezabezpečené připojení"),
|
||||
("Scale original", "Měřítko původní"),
|
||||
("Scale adaptive", "Měřítko adaptivní"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Afslut fuldskærm"),
|
||||
("Fullscreen", "Fuld skærm"),
|
||||
("Mobile Actions", "Mobile handlinger"),
|
||||
("Select Monitor", "Vælg Monitor"),
|
||||
("Control Actions", "Kontrolhandlinger"),
|
||||
("Display Settings", "Skærmindstillinger"),
|
||||
("Ratio", "Forhold"),
|
||||
("Image Quality", "Billede kvalitet"),
|
||||
("Scroll Style", "Rulstil"),
|
||||
("Show Menubar", "Vis menulinje"),
|
||||
("Hide Menubar", "skjul menulinjen"),
|
||||
("Direct Connection", "Direkte forbindelse"),
|
||||
("Relay Connection", "Relæforbindelse"),
|
||||
("Secure Connection", "Sikker forbindelse"),
|
||||
("Insecure Connection", "Usikker forbindelse"),
|
||||
("Scale original", "Skala original"),
|
||||
("Scale adaptive", "Skala adaptiv"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", "Entferntes Gerät wird neu gestartet"),
|
||||
("remote_restarting_tip", "Entferntes Gerät startet neu, bitte schließen Sie diese Meldung und verbinden Sie sich mit dem dauerhaften Passwort erneut."),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Vollbild beenden"),
|
||||
("Fullscreen", "Ganzer Bildschirm"),
|
||||
("Mobile Actions", "Mobile Aktionen"),
|
||||
("Select Monitor", "Wählen Sie Überwachen aus"),
|
||||
("Control Actions", "Kontrollaktionen"),
|
||||
("Display Settings", "Bildschirmeinstellungen"),
|
||||
("Ratio", "Verhältnis"),
|
||||
("Image Quality", "Bildqualität"),
|
||||
("Scroll Style", "Scroll-Stil"),
|
||||
("Show Menubar", "Menüleiste anzeigen"),
|
||||
("Hide Menubar", "Menüleiste ausblenden"),
|
||||
("Direct Connection", "Direkte Verbindung"),
|
||||
("Relay Connection", "Relaisverbindung"),
|
||||
("Secure Connection", "Sichere Verbindung"),
|
||||
("Insecure Connection", "Unsichere Verbindung"),
|
||||
("Scale original", "Original skalieren"),
|
||||
("Scale adaptive", "Adaptiv skalieren"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Eliru Plenekranon"),
|
||||
("Fullscreen", "Plenekrane"),
|
||||
("Mobile Actions", "Poŝtelefonaj Agoj"),
|
||||
("Select Monitor", "Elektu Monitoron"),
|
||||
("Control Actions", "Kontrolaj Agoj"),
|
||||
("Display Settings", "Montraj Agordoj"),
|
||||
("Ratio", "Proporcio"),
|
||||
("Image Quality", "Bilda Kvalito"),
|
||||
("Scroll Style", "Ruluma Stilo"),
|
||||
("Show Menubar", "Montru menubreton"),
|
||||
("Hide Menubar", "kaŝi menubreton"),
|
||||
("Direct Connection", "Rekta Konekto"),
|
||||
("Relay Connection", "Relajsa Konekto"),
|
||||
("Secure Connection", "Sekura Konekto"),
|
||||
("Insecure Connection", "Nesekura Konekto"),
|
||||
("Scale original", "Skalo originalo"),
|
||||
("Scale adaptive", "Skalo adapta"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -318,5 +318,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", "Reiniciando dispositivo remoto"),
|
||||
("remote_restarting_tip", "Dispositivo remoto reiniciando, favor de cerrar este mensaje y reconectarse con la contraseña permamente despues de un momento."),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Salir de pantalla completa"),
|
||||
("Fullscreen", "Pantalla completa"),
|
||||
("Mobile Actions", "Acciones móviles"),
|
||||
("Select Monitor", "Seleccionar monitor"),
|
||||
("Control Actions", "Acciones de control"),
|
||||
("Display Settings", "Configuración de pantalla"),
|
||||
("Ratio", "Relación"),
|
||||
("Image Quality", "La calidad de imagen"),
|
||||
("Scroll Style", "Estilo de desplazamiento"),
|
||||
("Show Menubar", "ajustes de pantalla"),
|
||||
("Hide Menubar", "ocultar barra de menú"),
|
||||
("Direct Connection", "Conexión directa"),
|
||||
("Relay Connection", "Conexión de relé"),
|
||||
("Secure Connection", "Conexión segura"),
|
||||
("Insecure Connection", "Conexión insegura"),
|
||||
("Scale original", "escala originales"),
|
||||
("Scale adaptive", "Adaptable a escala"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Quitter le mode plein écran"),
|
||||
("Fullscreen", "Plein écran"),
|
||||
("Mobile Actions", "Actions mobiles"),
|
||||
("Select Monitor", "Sélectionnez Moniteur"),
|
||||
("Control Actions", "Actions de contrôle"),
|
||||
("Display Settings", "Paramètres d'affichage"),
|
||||
("Ratio", "Rapport"),
|
||||
("Image Quality", "Qualité d'image"),
|
||||
("Scroll Style", "Style de défilement"),
|
||||
("Show Menubar", "Afficher la barre de menus"),
|
||||
("Hide Menubar", "masquer la barre de menus"),
|
||||
("Direct Connection", "Connexion directe"),
|
||||
("Relay Connection", "Connexion relais"),
|
||||
("Secure Connection", "Connexion sécurisée"),
|
||||
("Insecure Connection", "Connexion non sécurisée"),
|
||||
("Scale original", "Échelle d'origine"),
|
||||
("Scale adaptive", "Échelle adaptative"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Lépjen ki a teljes képernyőről"),
|
||||
("Fullscreen", "Teljes képernyő"),
|
||||
("Mobile Actions", "mobil műveletek"),
|
||||
("Select Monitor", "Válassza a Monitor lehetőséget"),
|
||||
("Control Actions", "Irányítási műveletek"),
|
||||
("Display Settings", "Megjelenítési beállítások"),
|
||||
("Ratio", "Hányados"),
|
||||
("Image Quality", "Képminőség"),
|
||||
("Scroll Style", "Görgetési stílus"),
|
||||
("Show Menubar", "Menüsor megjelenítése"),
|
||||
("Hide Menubar", "menüsor elrejtése"),
|
||||
("Direct Connection", "Közvetlen kapcsolat"),
|
||||
("Relay Connection", "Relé csatlakozás"),
|
||||
("Secure Connection", "Biztonságos kapcsolat"),
|
||||
("Insecure Connection", "Nem biztonságos kapcsolat"),
|
||||
("Scale original", "Eredeti méretarány"),
|
||||
("Scale adaptive", "Skála adaptív"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -318,5 +318,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", "Memulai Ulang Perangkat Jarak Jauh"),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Keluar dari Layar Penuh"),
|
||||
("Fullscreen", "Layar penuh"),
|
||||
("Mobile Actions", "Tindakan Seluler"),
|
||||
("Select Monitor", "Pilih Monitor"),
|
||||
("Control Actions", "Tindakan Kontrol"),
|
||||
("Display Settings", "Pengaturan tampilan"),
|
||||
("Ratio", "Perbandingan"),
|
||||
("Image Quality", "Kualitas gambar"),
|
||||
("Scroll Style", "Gaya Gulir"),
|
||||
("Show Menubar", "Tampilkan bilah menu"),
|
||||
("Hide Menubar", "sembunyikan bilah menu"),
|
||||
("Direct Connection", "Koneksi langsung"),
|
||||
("Relay Connection", "Koneksi Relay"),
|
||||
("Secure Connection", "Koneksi aman"),
|
||||
("Insecure Connection", "Koneksi Tidak Aman"),
|
||||
("Scale original", "Skala asli"),
|
||||
("Scale adaptive", "Skala adaptif"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -301,6 +301,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Are you sure you want to restart", "Sei sicuro di voler riavviare?"),
|
||||
("Restarting Remote Device", "Il dispositivo remoto si sta riavviando"),
|
||||
("remote_restarting_tip", "Riavviare il dispositivo remoto"),
|
||||
("Exit Fullscreen", "Esci dalla modalità schermo intero"),
|
||||
("Fullscreen", "A schermo intero"),
|
||||
("Mobile Actions", "Azioni mobili"),
|
||||
("Select Monitor", "Seleziona Monitora"),
|
||||
("Control Actions", "Azioni di controllo"),
|
||||
("Display Settings", "Impostazioni di visualizzazione"),
|
||||
("Ratio", "Rapporto"),
|
||||
("Image Quality", "Qualità dell'immagine"),
|
||||
("Scroll Style", "Stile di scorrimento"),
|
||||
("Show Menubar", "Mostra la barra dei menu"),
|
||||
("Hide Menubar", "nascondi la barra dei menu"),
|
||||
("Direct Connection", "Connessione diretta"),
|
||||
("Relay Connection", "Collegamento a relè"),
|
||||
("Secure Connection", "Connessione sicura"),
|
||||
("Insecure Connection", "Connessione insicura"),
|
||||
("Scale original", "Scala originale"),
|
||||
("Scale adaptive", "Scala adattiva"),
|
||||
("Legacy mode", ""),
|
||||
("Map mode", ""),
|
||||
("Translate mode", ""),
|
||||
|
@ -302,5 +302,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Are you sure you want to restart", "本当に再起動しますか"),
|
||||
("Restarting Remote Device", "リモート端末を再起動中"),
|
||||
("remote_restarting_tip", "リモート端末は再起動中です。このメッセージボックスを閉じて、しばらくした後に固定のパスワードを使用して再接続してください。"),
|
||||
("Exit Fullscreen", "全画面表示を終了"),
|
||||
("Fullscreen", "全画面表示"),
|
||||
("Mobile Actions", "モバイル アクション"),
|
||||
("Select Monitor", "モニターを選択"),
|
||||
("Control Actions", "コントロール アクション"),
|
||||
("Display Settings", "ディスプレイの設定"),
|
||||
("Ratio", "比率"),
|
||||
("Image Quality", "画質"),
|
||||
("Scroll Style", "スクロール スタイル"),
|
||||
("Show Menubar", "メニューバーを表示"),
|
||||
("Hide Menubar", "メニューバーを隠す"),
|
||||
("Direct Connection", "直接接続"),
|
||||
("Relay Connection", "リレー接続"),
|
||||
("Secure Connection", "安全な接続"),
|
||||
("Insecure Connection", "安全でない接続"),
|
||||
("Scale original", "オリジナルサイズ"),
|
||||
("Scale adaptive", "フィットウィンドウ"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -299,5 +299,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Are you sure you want to restart", "정말로 재시작 하시겠습니까"),
|
||||
("Restarting Remote Device", "원격 기기를 다시 시작하는중"),
|
||||
("remote_restarting_tip", "원격 장치를 다시 시작하는 중입니다. 이 메시지 상자를 닫고 잠시 후 영구 비밀번호로 다시 연결하십시오."),
|
||||
("Exit Fullscreen", "전체 화면 종료"),
|
||||
("Fullscreen", "전체화면"),
|
||||
("Mobile Actions", "모바일 액션"),
|
||||
("Select Monitor", "모니터 선택"),
|
||||
("Control Actions", "제어 작업"),
|
||||
("Display Settings", "화면 설정"),
|
||||
("Ratio", "비율"),
|
||||
("Image Quality", "이미지 품질"),
|
||||
("Scroll Style", "스크롤 스타일"),
|
||||
("Show Menubar", "메뉴 표시줄 표시"),
|
||||
("Hide Menubar", "메뉴 표시줄 숨기기"),
|
||||
("Direct Connection", "직접 연결"),
|
||||
("Relay Connection", "릴레이 연결"),
|
||||
("Secure Connection", "보안 연결"),
|
||||
("Insecure Connection", "안전하지 않은 연결"),
|
||||
("Scale original", "원래 크기"),
|
||||
("Scale adaptive", "맞는 창"),
|
||||
].iter().cloned().collect();
|
||||
}
|
@ -303,5 +303,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Set security password", "Ustaw hasło zabezpieczające"),
|
||||
("Connection not allowed", "Połączenie niedozwolone"),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Wyłączyć tryb pełnoekranowy"),
|
||||
("Fullscreen", "Pełny ekran"),
|
||||
("Mobile Actions", "Działania mobilne"),
|
||||
("Select Monitor", "Wybierz Monitor"),
|
||||
("Control Actions", "Działania kontrolne"),
|
||||
("Display Settings", "Ustawienia wyświetlania"),
|
||||
("Ratio", "Stosunek"),
|
||||
("Image Quality", "Jakość obrazu"),
|
||||
("Scroll Style", "Styl przewijania"),
|
||||
("Show Menubar", "Pokaż pasek menu"),
|
||||
("Hide Menubar", "ukryj pasek menu"),
|
||||
("Direct Connection", "Bezpośrednie połączenie"),
|
||||
("Relay Connection", "Połączenie przekaźnika"),
|
||||
("Secure Connection", "Bezpieczne połączenie"),
|
||||
("Insecure Connection", "Niepewne połączenie"),
|
||||
("Scale original", "Skala oryginalna"),
|
||||
("Scale adaptive", "Skala adaptacyjna"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -299,5 +299,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Are you sure you want to restart", "Tem a certeza que pretende reiniciar"),
|
||||
("Restarting Remote Device", "A reiniciar sistema remoto"),
|
||||
("remote_restarting_tip", ""),
|
||||
("Exit Fullscreen", "Sair da tela cheia"),
|
||||
("Fullscreen", "Tela cheia"),
|
||||
("Mobile Actions", "Ações para celular"),
|
||||
("Select Monitor", "Selecionar monitor"),
|
||||
("Control Actions", "Ações de controle"),
|
||||
("Display Settings", "Configurações do visor"),
|
||||
("Ratio", "Razão"),
|
||||
("Image Quality", "Qualidade da imagem"),
|
||||
("Scroll Style", "Estilo de rolagem"),
|
||||
("Show Menubar", "Mostrar barra de menus"),
|
||||
("Hide Menubar", "ocultar barra de menu"),
|
||||
("Direct Connection", "Conexão direta"),
|
||||
("Relay Connection", "Conexão de relé"),
|
||||
("Secure Connection", "Conexão segura"),
|
||||
("Insecure Connection", "Conexão insegura"),
|
||||
("Scale original", "Escala original"),
|
||||
("Scale adaptive", "Escala adaptável"),
|
||||
].iter().cloned().collect();
|
||||
}
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", ""),
|
||||
("Fullscreen", ""),
|
||||
("Mobile Actions", ""),
|
||||
("Select Monitor", ""),
|
||||
("Control Actions", ""),
|
||||
("Display Settings", ""),
|
||||
("Ratio", ""),
|
||||
("Image Quality", ""),
|
||||
("Scroll Style", ""),
|
||||
("Show Menubar", ""),
|
||||
("Hide Menubar", ""),
|
||||
("Direct Connection", ""),
|
||||
("Relay Connection", ""),
|
||||
("Secure Connection", ""),
|
||||
("Insecure Connection", ""),
|
||||
("Scale original", ""),
|
||||
("Scale adaptive", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", "Перезагрузка удаленного устройства"),
|
||||
("remote_restarting_tip", "Удаленное устройство перезапускается. Пожалуйста, закройте это сообщение и через некоторое время переподключитесь, используя постоянный пароль."),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Выйти из полноэкранного режима"),
|
||||
("Fullscreen", "Полноэкранный"),
|
||||
("Mobile Actions", "Мобильные действия"),
|
||||
("Select Monitor", "Выберите монитор"),
|
||||
("Control Actions", "Действия по управлению"),
|
||||
("Display Settings", "Настройки отображения"),
|
||||
("Ratio", "Соотношение"),
|
||||
("Image Quality", "Качество изображения"),
|
||||
("Scroll Style", "Стиль прокрутки"),
|
||||
("Show Menubar", "Показать строку меню"),
|
||||
("Hide Menubar", "скрыть строку меню"),
|
||||
("Direct Connection", "Прямая связь"),
|
||||
("Relay Connection", "Релейное соединение"),
|
||||
("Secure Connection", "Безопасное соединение"),
|
||||
("Insecure Connection", "Небезопасное соединение"),
|
||||
("Scale original", "Оригинал масштаба"),
|
||||
("Scale adaptive", "Масштаб адаптивный"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Ukončiť celú obrazovku"),
|
||||
("Fullscreen", "Celá obrazovka"),
|
||||
("Mobile Actions", "Mobilné akcie"),
|
||||
("Select Monitor", "Vyberte možnosť Monitor"),
|
||||
("Control Actions", "Kontrolné akcie"),
|
||||
("Display Settings", "Nastavenia displeja"),
|
||||
("Ratio", "Pomer"),
|
||||
("Image Quality", "Kvalita obrazu"),
|
||||
("Scroll Style", "Štýl posúvania"),
|
||||
("Show Menubar", "Zobraziť panel s ponukami"),
|
||||
("Hide Menubar", "skryť panel s ponukami"),
|
||||
("Direct Connection", "Priame pripojenie"),
|
||||
("Relay Connection", "Reléové pripojenie"),
|
||||
("Secure Connection", "Zabezpečené pripojenie"),
|
||||
("Insecure Connection", "Nezabezpečené pripojenie"),
|
||||
("Scale original", "Pôvodná mierka"),
|
||||
("Scale adaptive", "Prispôsobivá mierka"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", ""),
|
||||
("Fullscreen", ""),
|
||||
("Mobile Actions", ""),
|
||||
("Select Monitor", ""),
|
||||
("Control Actions", ""),
|
||||
("Display Settings", ""),
|
||||
("Ratio", ""),
|
||||
("Image Quality", ""),
|
||||
("Scroll Style", ""),
|
||||
("Show Menubar", ""),
|
||||
("Hide Menubar", ""),
|
||||
("Direct Connection", ""),
|
||||
("Relay Connection", ""),
|
||||
("Secure Connection", ""),
|
||||
("Insecure Connection", ""),
|
||||
("Scale original", ""),
|
||||
("Scale adaptive", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -318,5 +318,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", "Uzaktan yeniden başlatılıyor"),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Tam ekrandan çık"),
|
||||
("Fullscreen", "Tam ekran"),
|
||||
("Mobile Actions", "Mobil İşlemler"),
|
||||
("Select Monitor", "Monitörü Seç"),
|
||||
("Control Actions", "Kontrol Eylemleri"),
|
||||
("Display Settings", "Görüntü ayarları"),
|
||||
("Ratio", "Oran"),
|
||||
("Image Quality", "Görüntü kalitesi"),
|
||||
("Scroll Style", "Kaydırma Stili"),
|
||||
("Show Menubar", "Menü çubuğunu göster"),
|
||||
("Hide Menubar", "menü çubuğunu gizle"),
|
||||
("Direct Connection", "Doğrudan Bağlantı"),
|
||||
("Relay Connection", "Röle Bağlantısı"),
|
||||
("Secure Connection", "Güvenli bağlantı"),
|
||||
("Insecure Connection", "Güvenli Bağlantı"),
|
||||
("Scale original", "Orijinali ölçeklendir"),
|
||||
("Scale adaptive", "Ölçek uyarlanabilir"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", "正在重啓遠程設備"),
|
||||
("remote_restarting_tip", "遠程設備正在重啓,請關閉當前提示框,並在一段時間後使用永久密碼重新連接"),
|
||||
("Copied", "已複製"),
|
||||
("Exit Fullscreen", "退出全屏"),
|
||||
("Fullscreen", "全屏"),
|
||||
("Mobile Actions", "移動端操作"),
|
||||
("Select Monitor", "選擇監視器"),
|
||||
("Control Actions", "控制操作"),
|
||||
("Display Settings", "顯示設置"),
|
||||
("Ratio", "比例"),
|
||||
("Image Quality", "畫質"),
|
||||
("Scroll Style", "滾動樣式"),
|
||||
("Show Menubar", "顯示菜單欄"),
|
||||
("Hide Menubar", "隱藏菜單欄"),
|
||||
("Direct Connection", "直接連接"),
|
||||
("Relay Connection", "中繼連接"),
|
||||
("Secure Connection", "安全連接"),
|
||||
("Insecure Connection", "非安全連接"),
|
||||
("Scale original", "原始尺寸"),
|
||||
("Scale adaptive", "適應窗口"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", "Đang khởi động lại thiết bị từ xa"),
|
||||
("remote_restarting_tip", "Thiết bị từ xa đang khởi động lại, hãy đóng cửa sổ tin nhắn này và kết nối lại với mật khẩu vĩnh viễn sau một khoảng thời gian"),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Thoát toàn màn hình"),
|
||||
("Fullscreen", "Toàn màn hình"),
|
||||
("Mobile Actions", "Hành động trên thiết bị di động"),
|
||||
("Select Monitor", "Chọn màn hình"),
|
||||
("Control Actions", "Kiểm soát hành động"),
|
||||
("Display Settings", "Thiết lập hiển thị"),
|
||||
("Ratio", "Tỉ lệ"),
|
||||
("Image Quality", "Chất lượng hình ảnh"),
|
||||
("Scroll Style", "Kiểu cuộn"),
|
||||
("Show Menubar", "Hiển thị thanh menu"),
|
||||
("Hide Menubar", "ẩn thanh menu"),
|
||||
("Direct Connection", "Kết nối trực tiếp"),
|
||||
("Relay Connection", "Kết nối chuyển tiếp"),
|
||||
("Secure Connection", "Kết nối an toàn"),
|
||||
("Insecure Connection", "Kết nối không an toàn"),
|
||||
("Scale original", "Quy mô gốc"),
|
||||
("Scale adaptive", "Quy mô thích ứng"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ mod port_forward;
|
||||
mod tray;
|
||||
|
||||
mod ui_interface;
|
||||
mod ui_session_interface;
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod clipboard_file;
|
||||
|
@ -1,3 +1,5 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use crate::client::*;
|
||||
use hbb_common::{
|
||||
allow_err, bail,
|
||||
@ -48,6 +50,9 @@ pub async fn listen(
|
||||
ui_receiver: mpsc::UnboundedReceiver<Data>,
|
||||
key: &str,
|
||||
token: &str,
|
||||
lc: Arc<RwLock<LoginConfigHandler>>,
|
||||
remote_host: String,
|
||||
remote_port: i32,
|
||||
) -> ResultType<()> {
|
||||
let listener = tcp::new_listener(format!("0.0.0.0:{}", port), true).await?;
|
||||
let addr = listener.local_addr()?;
|
||||
@ -61,6 +66,7 @@ pub async fn listen(
|
||||
tokio::select! {
|
||||
Ok((forward, addr)) = listener.accept() => {
|
||||
log::info!("new connection from {:?}", addr);
|
||||
lc.write().unwrap().port_forward = (remote_host.clone(), remote_port);
|
||||
let id = id.clone();
|
||||
let password = password.clone();
|
||||
let mut forward = Framed::new(forward, BytesCodec::new());
|
||||
|
@ -950,6 +950,7 @@ impl Connection {
|
||||
addr
|
||||
))
|
||||
.await;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ pub fn start(args: &mut [String]) {
|
||||
let args: Vec<String> = iter.map(|x| x.clone()).collect();
|
||||
frame.set_title(&id);
|
||||
frame.register_behavior("native-remote", move || {
|
||||
Box::new(remote::Handler::new(
|
||||
Box::new(remote::SciterSession::new(
|
||||
cmd.clone(),
|
||||
id.clone(),
|
||||
pass.clone(),
|
||||
|
2688
src/ui/remote.rs
2688
src/ui/remote.rs
File diff suppressed because it is too large
Load Diff
1305
src/ui_session_interface.rs
Normal file
1305
src/ui_session_interface.rs
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user