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

2
flutter/.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,348 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:wakelock/wakelock.dart';
const double _kColumn1Width = 30;
const double _kColumn4Width = 100;
const double _kRowHeight = 50;
const double _kTextLeftMargin = 20;
class _PortForward {
int localPort;
String remoteHost;
int remotePort;
_PortForward.fromJson(List<dynamic> json)
: localPort = json[0] as int,
remoteHost = json[1] as String,
remotePort = json[2] as int;
}
class PortForwardPage extends StatefulWidget {
const PortForwardPage({Key? key, required this.id, required this.isRDP})
: super(key: key);
final String id;
final bool isRDP;
@override
State<PortForwardPage> createState() => _PortForwardPageState();
}
class _PortForwardPageState extends State<PortForwardPage>
with AutomaticKeepAliveClientMixin {
final bool isRdp = false;
final TextEditingController localPortController = TextEditingController();
final TextEditingController remoteHostController = TextEditingController();
final TextEditingController remotePortController = TextEditingController();
RxList<_PortForward> pfs = RxList.empty(growable: true);
late FFI _ffi;
@override
void initState() {
super.initState();
_ffi = FFI();
_ffi.connect(widget.id, isPortForward: true);
Get.put(_ffi, tag: 'pf_${widget.id}');
if (!Platform.isLinux) {
Wakelock.enable();
}
print("init success with id ${widget.id}");
}
@override
void dispose() {
_ffi.close();
_ffi.dialogManager.dismissAll();
if (!Platform.isLinux) {
Wakelock.disable();
}
Get.delete<FFI>(tag: 'pf_${widget.id}');
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
backgroundColor: MyTheme.color(context).grayBg,
body: FutureBuilder(future: () async {
if (!isRdp) {
refreshTunnelConfig();
}
}(), builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Container(
decoration: BoxDecoration(
border: Border.all(
width: 20, color: MyTheme.color(context).grayBg!)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
buildPrompt(context),
Flexible(
child: Container(
decoration: BoxDecoration(
color: MyTheme.color(context).bg,
border: Border.all(width: 1, color: MyTheme.border)),
child:
widget.isRDP ? buildRdp(context) : buildTunnel(context),
),
),
],
),
);
}
return const Offstage();
}),
);
}
buildPrompt(BuildContext context) {
return Obx(() => Offstage(
offstage: pfs.isEmpty && !widget.isRDP,
child: Container(
height: 45,
color: const Color(0xFF007F00),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
translate('Listening ...'),
style: const TextStyle(fontSize: 16, color: Colors.white),
),
Text(
translate('not_close_tcp_tip'),
style: const TextStyle(
fontSize: 10, color: Color(0xFFDDDDDD), height: 1.2),
)
])).marginOnly(bottom: 8),
));
}
buildTunnel(BuildContext context) {
text(String lable) => Expanded(
child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin));
return Theme(
data: Theme.of(context)
.copyWith(backgroundColor: MyTheme.color(context).bg),
child: Obx(() => ListView.builder(
itemCount: pfs.length + 2,
itemBuilder: ((context, index) {
if (index == 0) {
return Container(
height: 25,
color: MyTheme.color(context).grayBg,
child: Row(children: [
text('Local Port'),
const SizedBox(width: _kColumn1Width),
text('Remote Host'),
text('Remote Port'),
SizedBox(
width: _kColumn4Width, child: Text(translate('Action')))
]),
);
} else if (index == 1) {
return buildTunnelAddRow(context);
} else {
return buildTunnelDataRow(context, pfs[index - 2], index - 2);
}
}))),
);
}
buildTunnelAddRow(BuildContext context) {
var portInputFormatter = [
FilteringTextInputFormatter.allow(RegExp(
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$'))
];
return Container(
height: _kRowHeight,
decoration: BoxDecoration(color: MyTheme.color(context).bg),
child: Row(children: [
buildTunnelInputCell(context,
controller: localPortController,
inputFormatters: portInputFormatter),
const SizedBox(
width: _kColumn1Width, child: Icon(Icons.arrow_forward_sharp)),
buildTunnelInputCell(context,
controller: remoteHostController, hint: 'localhost'),
buildTunnelInputCell(context,
controller: remotePortController,
inputFormatters: portInputFormatter),
SizedBox(
width: _kColumn4Width,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0, side: const BorderSide(color: MyTheme.border)),
onPressed: () async {
int? localPort = int.tryParse(localPortController.text);
int? remotePort = int.tryParse(remotePortController.text);
if (localPort != null &&
remotePort != null &&
(remoteHostController.text.isEmpty ||
remoteHostController.text.trim().isNotEmpty)) {
await bind.sessionAddPortForward(
id: 'pf_${widget.id}',
localPort: localPort,
remoteHost: remoteHostController.text.trim().isEmpty
? 'localhost'
: remoteHostController.text.trim(),
remotePort: remotePort);
localPortController.clear();
remoteHostController.clear();
remotePortController.clear();
refreshTunnelConfig();
}
},
child: Text(
translate('Add'),
),
).marginAll(10),
),
]),
);
}
buildTunnelInputCell(BuildContext context,
{required TextEditingController controller,
List<TextInputFormatter>? inputFormatters,
String? hint}) {
return Expanded(
child: TextField(
controller: controller,
inputFormatters: inputFormatters,
cursorColor: MyTheme.color(context).text,
cursorHeight: 20,
cursorWidth: 1,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: MyTheme.color(context).border!)),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: MyTheme.color(context).border!)),
fillColor: MyTheme.color(context).bg,
contentPadding: const EdgeInsets.all(10),
hintText: hint,
hintStyle: TextStyle(
color: MyTheme.color(context).placeholder, fontSize: 16)),
style: TextStyle(color: MyTheme.color(context).text, fontSize: 16),
).marginAll(10),
);
}
Widget buildTunnelDataRow(BuildContext context, _PortForward pf, int index) {
text(String lable) => Expanded(
child: Text(lable, style: const TextStyle(fontSize: 20))
.marginOnly(left: _kTextLeftMargin));
return Container(
height: _kRowHeight,
decoration: BoxDecoration(
color: index % 2 == 0
? isDarkTheme()
? const Color(0xFF202020)
: const Color(0xFFF4F5F6)
: MyTheme.color(context).bg),
child: Row(children: [
text(pf.localPort.toString()),
const SizedBox(width: _kColumn1Width),
text(pf.remoteHost),
text(pf.remotePort.toString()),
SizedBox(
width: _kColumn4Width,
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () async {
await bind.sessionRemovePortForward(
id: 'pf_${widget.id}', localPort: pf.localPort);
refreshTunnelConfig();
},
),
),
]),
);
}
void refreshTunnelConfig() async {
String peer = await bind.mainGetPeer(id: widget.id);
Map<String, dynamic> config = jsonDecode(peer);
List<dynamic> infos = config['port_forwards'] as List;
List<_PortForward> result = List.empty(growable: true);
for (var e in infos) {
result.add(_PortForward.fromJson(e));
}
pfs.value = result;
}
buildRdp(BuildContext context) {
text1(String lable) =>
Expanded(child: Text(lable).marginOnly(left: _kTextLeftMargin));
text2(String lable) => Expanded(
child: Text(
lable,
style: TextStyle(fontSize: 20),
).marginOnly(left: _kTextLeftMargin));
return Theme(
data: Theme.of(context)
.copyWith(backgroundColor: MyTheme.color(context).bg),
child: ListView.builder(
itemCount: 2,
itemBuilder: ((context, index) {
if (index == 0) {
return Container(
height: 25,
color: MyTheme.color(context).grayBg,
child: Row(children: [
text1('Local Port'),
const SizedBox(width: _kColumn1Width),
text1('Remote Host'),
text1('Remote Port'),
]),
);
} else {
return Container(
height: _kRowHeight,
decoration: BoxDecoration(color: MyTheme.color(context).bg),
child: Row(children: [
Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: SizedBox(
width: 120,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
side: const BorderSide(color: MyTheme.border)),
onPressed: () {},
child: Text(
translate('New RDP'),
style: TextStyle(
fontWeight: FontWeight.w300, fontSize: 14),
),
).marginSymmetric(vertical: 10),
).marginOnly(left: 20),
),
),
const SizedBox(
width: _kColumn1Width,
child: Icon(Icons.arrow_forward_sharp)),
text2('localhost'),
text2('RDP'),
]),
);
}
})),
);
}
@override
bool get wantKeepAlive => true;
}

View File

@@ -0,0 +1,103 @@
import 'dart:convert';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/port_forward_page.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart';
class PortForwardTabPage extends StatefulWidget {
final Map<String, dynamic> params;
const PortForwardTabPage({Key? key, required this.params}) : super(key: key);
@override
State<PortForwardTabPage> createState() => _PortForwardTabPageState(params);
}
class _PortForwardTabPageState extends State<PortForwardTabPage> {
final tabController = Get.put(DesktopTabController());
late final bool isRDP;
static const IconData selectedIcon = Icons.forward_sharp;
static const IconData unselectedIcon = Icons.forward_outlined;
_PortForwardTabPageState(Map<String, dynamic> params) {
isRDP = params['isRDP'];
tabController.add(TabInfo(
key: params['id'],
label: params['id'],
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
page: PortForwardPage(
key: ValueKey(params['id']),
id: params['id'],
isRDP: isRDP,
)));
}
@override
void initState() {
super.initState();
tabController.onRemove = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
debugPrint(
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
// for simplify, just replace connectionId
if (call.method == "new_port_forward") {
final args = jsonDecode(call.arguments);
final id = args['id'];
final isRDP = args['isRDP'];
window_on_top(windowId());
tabController.add(TabInfo(
key: id,
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
page: PortForwardPage(id: id, isRDP: isRDP)));
} else if (call.method == "onDestroy") {
tabController.clear();
}
});
}
@override
Widget build(BuildContext context) {
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
return SubWindowDragToResizeArea(
windowId: windowId(),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: MyTheme.color(context).bg,
body: DesktopTab(
controller: tabController,
theme: theme,
tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward,
onClose: () {
tabController.clear();
},
tail: AddButton(
theme: theme,
).paddingOnly(left: 10),
)),
),
);
}
void onRemoveId(String id) {
ffi("pf_$id").close();
if (tabController.state.value.tabs.isEmpty) {
WindowController.fromWindowId(windowId()).hide();
}
}
int windowId() {
return widget.params["windowId"];
}
}

View File

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

View File

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

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/port_forward_tab_page.dart';
import 'package:provider/provider.dart';
/// multi-tab file port forward screen
class DesktopPortForwardScreen extends StatelessWidget {
final Map<String, dynamic> params;
const DesktopPortForwardScreen({Key? key, required this.params})
: super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: gFFI.ffiModel),
],
child: Scaffold(
body: PortForwardTabPage(
params: params,
),
),
);
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -21,18 +21,16 @@ final peerSearchTextController =
TextEditingController(text: peerSearchText.value);
class _PeerWidget extends StatefulWidget {
late final _peers;
late final OffstageFunc _offstageFunc;
late final PeerCardWidgetFunc _peerCardWidgetFunc;
final Peers peers;
final OffstageFunc offstageFunc;
final PeerCardWidgetFunc peerCardWidgetFunc;
_PeerWidget(Peers peers, OffstageFunc offstageFunc,
PeerCardWidgetFunc peerCardWidgetFunc,
{Key? key})
: super(key: key) {
_peers = peers;
_offstageFunc = offstageFunc;
_peerCardWidgetFunc = peerCardWidgetFunc;
}
const _PeerWidget(
{required this.peers,
required this.offstageFunc,
required this.peerCardWidgetFunc,
Key? key})
: super(key: key);
@override
_PeerWidgetState createState() => _PeerWidgetState();
@@ -42,9 +40,9 @@ class _PeerWidget extends StatefulWidget {
class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
static const int _maxQueryCount = 3;
var _curPeers = Set<String>();
final _curPeers = <String>{};
var _lastChangeTime = DateTime.now();
var _lastQueryPeers = Set<String>();
var _lastQueryPeers = <String>{};
var _lastQueryTime = DateTime.now().subtract(Duration(hours: 1));
var _queryCoun = 0;
var _exit = false;
@@ -78,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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'dart:async';
import 'dart:math';
import 'package:desktop_multi_window/desktop_multi_window.dart';
@@ -5,9 +7,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
import 'package:scroll_pos/scroll_pos.dart';
import 'package:window_manager/window_manager.dart';
import '../../utils/multi_window_manager.dart';
@@ -33,6 +36,15 @@ class TabInfo {
required this.page});
}
enum DesktopTabType {
main,
cm,
remoteScreen,
fileTransfer,
portForward,
rdp,
}
class DesktopTabState {
final List<TabInfo> tabs = [];
final ScrollPosController scrollController =
@@ -63,6 +75,7 @@ class DesktopTabController {
state.update((val) {
val!.tabs.add(tab);
});
state.value.scrollController.itemCount = state.value.tabs.length;
toIndex = state.value.tabs.length - 1;
assert(toIndex >= 0);
}
@@ -95,8 +108,16 @@ class DesktopTabController {
void jumpTo(int index) {
state.update((val) {
val!.selected = index;
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

View File

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

View File

@@ -7,6 +7,7 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/generated_bridge.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart';
@@ -17,6 +18,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:tuple/tuple.dart';
import '../common.dart';
import '../common/shared_state.dart';
import '../mobile/widgets/dialog.dart';
import '../mobile/widgets/overlay.dart';
import 'peer_model.dart';
@@ -96,25 +98,26 @@ class FfiModel with ChangeNotifier {
clearPermissions();
}
void setConnectionType(bool secure, bool direct) {
void setConnectionType(String peerId, bool secure, bool direct) {
_secure = secure;
_direct = direct;
try {
var connectionType = ConnectionTypeState.find(peerId);
connectionType.setSecure(secure);
connectionType.setDirect(direct);
} catch (e) {
//
}
}
Image? getConnectionImage() {
String? icon;
if (secure == true && direct == true) {
icon = 'secure';
} else if (secure == false && direct == true) {
icon = 'insecure';
} else if (secure == false && direct == false) {
icon = 'insecure_relay';
} else if (secure == true && direct == false) {
icon = 'secure_relay';
if (secure == null || direct == null) {
return null;
} else {
final icon =
'${secure == true ? "secure" : "insecure"}${direct == true ? "" : "_relay"}';
return Image.asset('assets/$icon.png', width: 48, height: 48);
}
return icon == null
? null
: Image.asset('assets/$icon.png', width: 48, height: 48);
}
void clearPermissions() {
@@ -130,7 +133,8 @@ class FfiModel with ChangeNotifier {
} else if (name == 'peer_info') {
handlePeerInfo(evt, peerId);
} else if (name == 'connection_ready') {
setConnectionType(evt['secure'] == 'true', evt['direct'] == 'true');
setConnectionType(
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
} else if (name == 'switch_display') {
handleSwitchDisplay(evt);
} else if (name == 'cursor_data') {
@@ -172,9 +176,9 @@ class FfiModel with ChangeNotifier {
} else if (name == 'update_quality_status') {
parent.target?.qualityMonitorModel.updateQualityStatus(evt);
} else if (name == 'update_block_input_state') {
updateBlockInputState(evt);
updateBlockInputState(evt, peerId);
} else if (name == 'update_privacy_mode') {
updatePrivacyMode(evt);
updatePrivacyMode(evt, peerId);
}
};
}
@@ -189,7 +193,7 @@ class FfiModel with ChangeNotifier {
handlePeerInfo(evt, peerId);
} else if (name == 'connection_ready') {
parent.target?.ffiModel.setConnectionType(
evt['secure'] == 'true', evt['direct'] == 'true');
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
} else if (name == 'switch_display') {
handleSwitchDisplay(evt);
} else if (name == 'cursor_data') {
@@ -231,9 +235,9 @@ class FfiModel with ChangeNotifier {
} else if (name == 'update_quality_status') {
parent.target?.qualityMonitorModel.updateQualityStatus(evt);
} else if (name == 'update_block_input_state') {
updateBlockInputState(evt);
updateBlockInputState(evt, peerId);
} else if (name == 'update_privacy_mode') {
updatePrivacyMode(evt);
updatePrivacyMode(evt, peerId);
}
};
platformFFI.setEventCallback(cb);
@@ -297,6 +301,9 @@ class FfiModel with ChangeNotifier {
/// Handle the peer info event based on [evt].
void handlePeerInfo(Map<String, dynamic> evt, String peerId) async {
// recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
bind.mainLoadRecentPeers();
parent.target?.dialogManager.dismissAll();
_pi.version = evt['version'];
_pi.username = evt['username'];
@@ -305,6 +312,12 @@ class FfiModel with ChangeNotifier {
_pi.sasEnabled = evt['sas_enabled'] == "true";
_pi.currentDisplay = int.parse(evt['current_display']);
try {
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
} catch (e) {
//
}
if (isPeerAndroid) {
_touchMode = true;
if (parent.target?.ffiModel.permissions['keyboard'] != false) {
@@ -316,6 +329,7 @@ class FfiModel with ChangeNotifier {
}
if (evt['is_file_transfer'] == "true") {
// TODO is file transfer
parent.target?.fileModel.onReady();
} else {
_pi.displays = [];
@@ -343,13 +357,24 @@ class FfiModel with ChangeNotifier {
notifyListeners();
}
updateBlockInputState(Map<String, dynamic> evt) {
updateBlockInputState(Map<String, dynamic> evt, String peerId) {
_inputBlocked = evt['input_state'] == 'on';
notifyListeners();
try {
BlockInputState.find(peerId).value = evt['input_state'] == 'on';
} catch (e) {
//
}
}
updatePrivacyMode(Map<String, dynamic> evt) {
updatePrivacyMode(Map<String, dynamic> evt, String peerId) {
notifyListeners();
try {
PrivacyModeState.find(peerId).value =
bind.sessionGetToggleOptionSync(id: peerId, arg: 'privacy-mode');
} catch (e) {
//
}
}
}
@@ -476,39 +501,11 @@ class CanvasModel with ChangeNotifier {
return;
}
final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720);
final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280);
// Closure to perform shrink operation.
final shrinkOp = () {
final s = s1 < s2 ? s1 : s2;
if (s < 1) {
_scale = s;
}
};
// Closure to perform stretch operation.
final stretchOp = () {
final s = s1 < s2 ? s1 : s2;
if (s > 1) {
_scale = s;
}
};
// Closure to perform default operation(set the scale to 1.0).
final defaultOp = () {
_scale = 1.0;
};
// // On desktop, shrink is the default behavior.
// if (isDesktop) {
// shrinkOp();
// } else {
defaultOp();
// }
if (style == 'shrink') {
shrinkOp();
} else if (style == 'stretch') {
stretchOp();
_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 {

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
import 'dart:convert';
import 'dart:ui';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
@@ -35,10 +34,11 @@ class RustDeskMultiWindowManager {
int? _remoteDesktopWindowId;
int? _fileTransferWindowId;
int? _portForwardWindowId;
Future<dynamic> new_remote_desktop(String remote_id) async {
Future<dynamic> newRemoteDesktop(String remoteId) async {
final msg =
jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remote_id});
jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remoteId});
try {
final ids = await DesktopMultiWindow.getAllSubWindowIds();
@@ -62,9 +62,9 @@ class RustDeskMultiWindowManager {
}
}
Future<dynamic> new_file_transfer(String remote_id) async {
Future<dynamic> newFileTransfer(String remoteId) async {
final msg =
jsonEncode({"type": WindowType.FileTransfer.index, "id": remote_id});
jsonEncode({"type": WindowType.FileTransfer.index, "id": remoteId});
try {
final ids = await DesktopMultiWindow.getAllSubWindowIds();
@@ -87,6 +87,31 @@ class RustDeskMultiWindowManager {
}
}
Future<dynamic> newPortForward(String remoteId, bool isRDP) async {
final msg = jsonEncode(
{"type": WindowType.PortForward.index, "id": remoteId, "isRDP": isRDP});
try {
final ids = await DesktopMultiWindow.getAllSubWindowIds();
if (!ids.contains(_portForwardWindowId)) {
_portForwardWindowId = null;
}
} on Error {
_portForwardWindowId = null;
}
if (_portForwardWindowId == null) {
final portForwardController = await DesktopMultiWindow.createWindow(msg);
portForwardController
..setFrame(const Offset(0, 0) & const Size(1280, 720))
..center()
..setTitle("rustdesk - port forward")
..show();
_portForwardWindowId = portForwardController.windowId;
} else {
return call(WindowType.PortForward, "new_port_forward", msg);
}
}
Future<dynamic> call(WindowType type, String methodName, dynamic args) async {
int? windowId = findWindowByType(type);
if (windowId == null) {
@@ -104,7 +129,7 @@ class RustDeskMultiWindowManager {
case WindowType.FileTransfer:
return _fileTransferWindowId;
case WindowType.PortForward:
break;
return _portForwardWindowId;
case WindowType.Unknown:
break;
}
@@ -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;

View File

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

View File

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

View File

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