Merge remote-tracking branch 'rd/master' into feat/x11/clipboard-file/init

Signed-off-by: ClSlaid <cailue@bupt.edu.cn>
This commit is contained in:
ClSlaid
2023-10-26 18:16:08 +08:00
108 changed files with 2191 additions and 1084 deletions

View File

@@ -14,8 +14,8 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
@@ -53,6 +53,9 @@ int androidVersion = 0;
int windowsBuildNumber = 0;
DesktopType? desktopType;
bool get isMainDesktopWindow =>
desktopType == DesktopType.main || desktopType == DesktopType.cm;
/// Check if the app is running with single view mode.
bool isSingleViewApp() {
return desktopType == DesktopType.cm;
@@ -955,7 +958,7 @@ class CustomAlertDialog extends StatelessWidget {
void msgBox(SessionID sessionId, String type, String title, String text,
String link, OverlayDialogManager dialogManager,
{bool? hasCancel, ReconnectHandle? reconnect}) {
{bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
dialogManager.dismissAll();
List<Widget> buttons = [];
bool hasOk = false;
@@ -995,22 +998,21 @@ void msgBox(SessionID sessionId, String type, String title, String text,
dialogManager.dismissAll();
}));
}
if (reconnect != null && title == "Connection Error") {
if (reconnect != null &&
title == "Connection Error" &&
reconnectTimeout != null) {
// `enabled` is used to disable the dialog button once the button is clicked.
final enabled = true.obs;
final button = Obx(
() => dialogButton(
'Reconnect',
isOutline: true,
onPressed: enabled.isTrue
? () {
// Disable the button
enabled.value = false;
reconnect(dialogManager, sessionId, false);
}
: null,
),
);
final button = Obx(() => _ReconnectCountDownButton(
second: reconnectTimeout,
onPressed: enabled.isTrue
? () {
// Disable the button
enabled.value = false;
reconnect(dialogManager, sessionId, false);
}
: null,
));
buttons.insert(0, button);
}
if (link.isNotEmpty) {
@@ -1491,8 +1493,8 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
late Offset position;
late Size sz;
late bool isMaximized;
bool isFullscreen = stateGlobal.fullscreen ||
(Platform.isMacOS && stateGlobal.closeOnFullscreen);
bool isFullscreen = stateGlobal.fullscreen.isTrue ||
(Platform.isMacOS && stateGlobal.closeOnFullscreen == true);
setFrameIfMaximized() {
if (isMaximized) {
final pos = bind.getLocalFlutterOption(k: kWindowPrefix + type.name);
@@ -1670,8 +1672,10 @@ Future<Offset?> _adjustRestoreMainWindowOffset(
/// Restore window position and size on start
/// Note that windowId must be provided if it's subwindow
//
// display is used to set the offset of the window in individual display mode.
Future<bool> restoreWindowPosition(WindowType type,
{int? windowId, String? peerId}) async {
{int? windowId, String? peerId, int? display}) async {
if (bind
.mainGetEnv(key: "DISABLE_RUSTDESK_RESTORE_WINDOW_POSITION")
.isNotEmpty) {
@@ -1707,14 +1711,22 @@ Future<bool> restoreWindowPosition(WindowType type,
debugPrint("no window position saved, ignoring position restoration");
return false;
}
if (type == WindowType.RemoteDesktop &&
!isRemotePeerPos &&
windowId != null) {
if (lpos.offsetWidth != null) {
lpos.offsetWidth = lpos.offsetWidth! + windowId * 20;
if (type == WindowType.RemoteDesktop) {
if (!isRemotePeerPos && windowId != null) {
if (lpos.offsetWidth != null) {
lpos.offsetWidth = lpos.offsetWidth! + windowId * kNewWindowOffset;
}
if (lpos.offsetHeight != null) {
lpos.offsetHeight = lpos.offsetHeight! + windowId * kNewWindowOffset;
}
}
if (lpos.offsetHeight != null) {
lpos.offsetHeight = lpos.offsetHeight! + windowId * 20;
if (display != null) {
if (lpos.offsetWidth != null) {
lpos.offsetWidth = lpos.offsetWidth! + display * kNewWindowOffset;
}
if (lpos.offsetHeight != null) {
lpos.offsetHeight = lpos.offsetHeight! + display * kNewWindowOffset;
}
}
}
@@ -2012,6 +2024,10 @@ connect(
final idController = Get.find<IDTextEditingController>();
idController.text = formatID(id);
}
if (Get.isRegistered<TextEditingController>()){
final fieldTextEditingController = Get.find<TextEditingController>();
fieldTextEditingController.text = formatID(id);
}
} catch (_) {}
}
id = id.replaceAll(' ', '');
@@ -2605,3 +2621,183 @@ bool isChooseDisplayToOpenInNewWindow(PeerInfo pi, SessionID sessionId) =>
pi.isSupportMultiDisplay &&
useTextureRender &&
bind.sessionGetDisplaysAsIndividualWindows(sessionId: sessionId) == 'Y';
Future<List<Rect>> getScreenListWayland() async {
final screenRectList = <Rect>[];
if (isMainDesktopWindow) {
for (var screen in await window_size.getScreenList()) {
final scale = kIgnoreDpi ? 1.0 : screen.scaleFactor;
double l = screen.frame.left;
double t = screen.frame.top;
double r = screen.frame.right;
double b = screen.frame.bottom;
final rect = Rect.fromLTRB(l / scale, t / scale, r / scale, b / scale);
screenRectList.add(rect);
}
} else {
final screenList = await rustDeskWinManager.call(
WindowType.Main, kWindowGetScreenList, '');
try {
for (var screen in jsonDecode(screenList.result) as List<dynamic>) {
final scale = kIgnoreDpi ? 1.0 : screen['scaleFactor'];
double l = screen['frame']['l'];
double t = screen['frame']['t'];
double r = screen['frame']['r'];
double b = screen['frame']['b'];
final rect = Rect.fromLTRB(l / scale, t / scale, r / scale, b / scale);
screenRectList.add(rect);
}
} catch (e) {
debugPrint('Failed to parse screenList: $e');
}
}
return screenRectList;
}
Future<List<Rect>> getScreenListNotWayland() async {
final screenRectList = <Rect>[];
final displays = bind.mainGetDisplays();
if (displays.isEmpty) {
return screenRectList;
}
try {
for (var display in jsonDecode(displays) as List<dynamic>) {
// to-do: scale factor ?
// final scale = kIgnoreDpi ? 1.0 : screen.scaleFactor;
double l = display['x'].toDouble();
double t = display['y'].toDouble();
double r = (display['x'] + display['w']).toDouble();
double b = (display['y'] + display['h']).toDouble();
screenRectList.add(Rect.fromLTRB(l, t, r, b));
}
} catch (e) {
debugPrint('Failed to parse displays: $e');
}
return screenRectList;
}
Future<List<Rect>> getScreenRectList() async {
return bind.mainCurrentIsWayland()
? await getScreenListWayland()
: await getScreenListNotWayland();
}
openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi) {
final displays = i == kAllDisplayValue
? List.generate(pi.displays.length, (index) => index)
: [i];
bind.sessionSwitchDisplay(
sessionId: ffi.sessionId, value: Int32List.fromList(displays));
ffi.ffiModel.switchToNewDisplay(i, ffi.sessionId, ffi.id);
}
// Open new tab or window to show this monitor.
// For now just open new window.
//
// screenRect is used to move the new window to the specified screen and set fullscreen.
openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
{Rect? screenRect}) {
final args = {
'window_id': stateGlobal.windowId,
'peer_id': peerId,
'display': i,
'display_count': pi.displays.length,
};
if (screenRect != null) {
args['screen_rect'] = {
'l': screenRect.left,
't': screenRect.top,
'r': screenRect.right,
'b': screenRect.bottom,
};
}
DesktopMultiWindow.invokeMethod(
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
}
tryMoveToScreenAndSetFullscreen(Rect? screenRect) async {
if (screenRect == null) {
return;
}
final wc = WindowController.fromWindowId(stateGlobal.windowId);
final curFrame = await wc.getFrame();
final frame =
Rect.fromLTWH(screenRect.left + 30, screenRect.top + 30, 600, 400);
if (stateGlobal.fullscreen.isTrue &&
curFrame.left <= frame.left &&
curFrame.top <= frame.top &&
curFrame.width >= frame.width &&
curFrame.height >= frame.height) {
return;
}
await wc.setFrame(frame);
// An duration is needed to avoid the window being restored after fullscreen.
Future.delayed(Duration(milliseconds: 300), () async {
stateGlobal.setFullscreen(true);
});
}
parseParamScreenRect(Map<String, dynamic> params) {
Rect? screenRect;
if (params['screen_rect'] != null) {
double l = params['screen_rect']['l'];
double t = params['screen_rect']['t'];
double r = params['screen_rect']['r'];
double b = params['screen_rect']['b'];
screenRect = Rect.fromLTRB(l, t, r, b);
}
return screenRect;
}
class _ReconnectCountDownButton extends StatefulWidget {
_ReconnectCountDownButton({
Key? key,
required this.second,
required this.onPressed,
}) : super(key: key);
final VoidCallback? onPressed;
final int second;
@override
State<_ReconnectCountDownButton> createState() =>
_ReconnectCountDownButtonState();
}
class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
late int _countdownSeconds = widget.second;
Timer? _timer;
@override
void initState() {
super.initState();
_startCountdownTimer();
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void _startCountdownTimer() {
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (_countdownSeconds <= 0) {
timer.cancel();
} else {
setState(() {
_countdownSeconds--;
});
}
});
}
@override
Widget build(BuildContext context) {
return dialogButton(
'${translate('Reconnect')} (${_countdownSeconds}s)',
onPressed: widget.onPressed,
isOutline: true,
);
}
}

View File

@@ -0,0 +1,196 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import '../../../models/platform_model.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart';
Future<List<Peer>> getAllPeers() async {
Map<String, dynamic> recentPeers = jsonDecode(await bind.mainLoadRecentPeersSync());
Map<String, dynamic> lanPeers = jsonDecode(await bind.mainLoadLanPeersSync());
Map<String, dynamic> abPeers = jsonDecode(await bind.mainLoadAbSync());
Map<String, dynamic> groupPeers = jsonDecode(await bind.mainLoadGroupSync());
Map<String, dynamic> combinedPeers = {};
void _mergePeers(Map<String, dynamic> peers) {
if (peers.containsKey("peers")) {
dynamic peerData = peers["peers"];
if (peerData is String) {
try {
peerData = jsonDecode(peerData);
} catch (e) {
print("Error decoding peers: $e");
return;
}
}
if (peerData is List) {
for (var peer in peerData) {
if (peer is Map && peer.containsKey("id")) {
String id = peer["id"];
if (id != null && !combinedPeers.containsKey(id)) {
combinedPeers[id] = peer;
}
}
}
}
}
}
_mergePeers(recentPeers);
_mergePeers(lanPeers);
_mergePeers(abPeers);
_mergePeers(groupPeers);
List<Peer> parsedPeers = [];
for (var peer in combinedPeers.values) {
parsedPeers.add(Peer.fromJson(peer));
}
return parsedPeers;
}
class AutocompletePeerTile extends StatefulWidget {
final IDTextEditingController idController;
final Peer peer;
const AutocompletePeerTile({
Key? key,
required this.idController,
required this.peer,
}) : super(key: key);
@override
_AutocompletePeerTileState createState() => _AutocompletePeerTileState();
}
class _AutocompletePeerTileState extends State<AutocompletePeerTile>{
List _frontN<T>(List list, int n) {
if (list.length <= n) {
return list;
} else {
return list.sublist(0, n);
}
}
@override
Widget build(BuildContext context){
final double _tileRadius = 5;
final name =
'${widget.peer.username}${widget.peer.username.isNotEmpty && widget.peer.hostname.isNotEmpty ? '@' : ''}${widget.peer.hostname}';
final greyStyle = TextStyle(
fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
final child = GestureDetector(
onTap: () {
setState(() {
widget.idController.id = widget.peer.id;
FocusScope.of(context).unfocus();
});
},
child:
Container(
height: 42,
margin: EdgeInsets.only(bottom: 5),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Container(
decoration: BoxDecoration(
color: str2color('${widget.peer.id}${widget.peer.platform}', 0x7f),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(_tileRadius),
bottomLeft: Radius.circular(_tileRadius),
),
),
alignment: Alignment.center,
width: 42,
height: null,
child: Padding(
padding: EdgeInsets.all(6),
child: getPlatformImage(widget.peer.platform, size: 30)
)
),
Expanded(
child: Container(
padding: EdgeInsets.only(left: 10),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.only(
topRight: Radius.circular(_tileRadius),
bottomRight: Radius.circular(_tileRadius),
),
),
child: Row(
children: [
Expanded(
child: Container(
margin: EdgeInsets.only(top: 2),
child: Container(
margin: EdgeInsets.only(top: 2),
child: Column(
children: [
Container(
margin: EdgeInsets.only(top: 2),
child: Row(children: [
getOnline(8, widget.peer.online),
Expanded(
child: Text(
widget.peer.alias.isEmpty ? formatID(widget.peer.id) : widget.peer.alias,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall,
)),
!widget.peer.alias.isEmpty?
Padding(
padding: const EdgeInsets.only(left: 5, right: 5),
child: Text(
"(${widget.peer.id})",
style: greyStyle,
overflow: TextOverflow.ellipsis,
)
)
: Container(),
])),
Align(
alignment: Alignment.centerLeft,
child: Text(
name,
style: greyStyle,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
),
),
],
)
))),
],
)
),
)
],
)));
final colors =
_frontN(widget.peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
return Tooltip(
message: isMobile
? ''
: widget.peer.tags.isNotEmpty
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'
: '',
child: Stack(children: [
child,
if (colors.isNotEmpty)
Positioned(
top: 5,
right: 10,
child: CustomPaint(
painter: TagPainter(radius: 3, colors: colors),
),
)
]),
);
}
}

View File

@@ -19,7 +19,7 @@ import 'dart:math' as math;
typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
Function(BuildContext);
enum PeerUiType { grid, list }
enum PeerUiType { grid, tile, list }
final peerCardUiType = PeerUiType.grid.obs;

View File

@@ -215,29 +215,7 @@ class _PeerTabPageState extends State<PeerTabPage>
}
Widget _createPeerViewTypeSwitch(BuildContext context) {
final textColor = Theme.of(context).textTheme.titleLarge?.color;
final types = [PeerUiType.grid, PeerUiType.list];
return Obx(() => _hoverAction(
context: context,
onTap: () async {
final type = types
.elementAt(peerCardUiType.value == types.elementAt(0) ? 1 : 0);
await bind.setLocalFlutterOption(
k: 'peer-card-ui-type', v: type.index.toString());
peerCardUiType.value = type;
},
child: Tooltip(
message: peerCardUiType.value == PeerUiType.grid
? translate('List View')
: translate('Grid View'),
child: Icon(
peerCardUiType.value == PeerUiType.grid
? Icons.view_list_rounded
: Icons.grid_view_rounded,
size: 18,
color: textColor,
))));
return PeerViewDropdown();
}
Widget _createMultiSelection() {
@@ -777,6 +755,85 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
}
}
class PeerViewDropdown extends StatefulWidget {
const PeerViewDropdown({super.key});
@override
State<PeerViewDropdown> createState() => _PeerViewDropdownState();
}
class _PeerViewDropdownState extends State<PeerViewDropdown> {
RelativeRect menuPos = RelativeRect.fromLTRB(0, 0, 0, 0);
@override
Widget build(BuildContext context) {
final List<PeerUiType> types = [PeerUiType.grid, PeerUiType.tile, PeerUiType.list];
final style = TextStyle(
color: Theme.of(context).textTheme.titleLarge?.color,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal);
List<PopupMenuEntry> items = List.empty(growable: true);
items.add(PopupMenuItem(
height: 36,
enabled: false,
child: Text(translate("Change view"), style: style)));
for (var e in PeerUiType.values) {
items.add(PopupMenuItem(
height: 36,
child: Obx(() => Center(
child: SizedBox(
height: 36,
child: getRadio<PeerUiType>(
Text(translate(
types.indexOf(e) == 0 ? 'Big tiles' : types.indexOf(e) == 1 ? 'Small tiles' : 'List'
), style: style),
e,
peerCardUiType.value,
dense: true,
(PeerUiType? v) async {
if (v != null) {
peerCardUiType.value = v;
setState(() {});
await bind.setLocalFlutterOption(
k: "peer-card-ui-type",
v: peerCardUiType.value.index.toString(),
);
}}
),
),
))));
}
return _hoverAction(
context: context,
child: Tooltip(
message: translate('Change view'),
child: Icon(
peerCardUiType.value == PeerUiType.grid
? Icons.grid_view_rounded
: peerCardUiType.value == PeerUiType.tile
? Icons.view_list_rounded
: Icons.view_agenda_rounded,
size: 18,
)),
onTapDown: (details) {
final x = details.globalPosition.dx;
final y = details.globalPosition.dy;
setState(() {
menuPos = RelativeRect.fromLTRB(x, y, x, y);
});
},
onTap: () => showMenu(
context: context,
position: menuPos,
items: items,
elevation: 8,
),
);
}
}
class PeerSortDropdown extends StatefulWidget {
const PeerSortDropdown({super.key});

View File

@@ -9,6 +9,7 @@ import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import '../../common.dart';
import '../../models/peer_model.dart';
@@ -188,12 +189,19 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
onVisibilityChanged: onVisibilityChanged,
child: widget.peerCardBuilder(peer),
);
final windowWidth = MediaQuery.of(context).size.width;
final model = Provider.of<PeerTabModel>(context);
final hideAbTagsPanel = bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
return isDesktop
? Obx(
() => SizedBox(
width: 220,
width: peerCardUiType.value != PeerUiType.list
? 220
: model.currentTab == PeerTabIndex.group.index || (model.currentTab == PeerTabIndex.ab.index && !hideAbTagsPanel)
? windowWidth - 390 :
windowWidth - 227,
height:
peerCardUiType.value == PeerUiType.grid ? 140 : 42,
peerCardUiType.value == PeerUiType.grid ? 140 : peerCardUiType.value != PeerUiType.list ? 42 : 45,
child: visibilityChild,
),
)

View File

@@ -224,11 +224,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
));
}
// record
var codecFormat = ffi.qualityMonitorModel.data.codecFormat;
if (!isDesktop &&
(ffi.recordingModel.start ||
(perms["recording"] != false &&
(codecFormat == "VP8" || codecFormat == "VP9")))) {
(ffi.recordingModel.start || (perms["recording"] != false))) {
v.add(TTextMenu(
child: Row(
children: [
@@ -533,5 +530,20 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Show displays as individual windows'))));
}
final screenList = await getScreenRectList();
if (useTextureRender && pi.isSupportMultiDisplay && screenList.length > 1) {
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
sessionId: ffi.sessionId) ==
'Y';
v.add(TToggleMenu(
value: value,
onChanged: (value) {
if (value == null) return;
bind.sessionSetUseAllMyDisplaysForTheRemoteSession(
sessionId: sessionId, value: value ? 'Y' : '');
},
child: Text(translate('Use all my displays for the remote session'))));
}
return v;
}

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
const double kDesktopRemoteTabBarHeight = 28.0;
const int kInvalidWindowId = -1;
@@ -10,6 +11,10 @@ const int kMainWindowId = 0;
const kAllDisplayValue = -1;
const kKeyLegacyMode = 'legacy';
const kKeyMapMode = 'map';
const kKeyTranslateMode = 'translate';
const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS";
@@ -29,6 +34,7 @@ const String kAppTypeDesktopPortForward = "port forward";
const String kWindowMainWindowOnTop = "main_window_on_top";
const String kWindowGetWindowInfo = "get_window_info";
const String kWindowGetScreenList = "get_screen_list";
const String kWindowDisableGrabKeyboard = "disable_grab_keyboard";
const String kWindowActionRebuild = "rebuild";
const String kWindowEventHide = "hide";
@@ -64,7 +70,10 @@ const int kWindowMainId = 0;
const String kPointerEventKindTouch = "touch";
const String kPointerEventKindMouse = "mouse";
const String kKeyShowDisplaysAsIndividualWindows = 'displays_as_individual_windows';
const String kKeyShowDisplaysAsIndividualWindows =
'displays_as_individual_windows';
const String kKeyUseAllMyDisplaysForTheRemoteSession =
'use_all_my_displays_for_the_remote_session';
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
// the executable name of the portable version
@@ -84,9 +93,17 @@ const int kDesktopMaxDisplaySize = 3840;
const double kDesktopFileTransferRowHeight = 30.0;
const double kDesktopFileTransferHeaderHeight = 25.0;
double kNewWindowOffset = Platform.isWindows
? 56.0
: Platform.isLinux
? 50.0
: Platform.isMacOS
? 30.0
: 50.0;
EdgeInsets get kDragToResizeAreaPadding =>
!kUseCompatibleUiMode && Platform.isLinux
? stateGlobal.fullscreen || stateGlobal.isMaximized.value
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
? EdgeInsets.zero
: EdgeInsets.all(5.0)
: EdgeInsets.zero;

View File

@@ -11,10 +11,12 @@ import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import '../../common.dart';
import '../../common/formatter/id_formatter.dart';
import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/autocomplete.dart';
import '../../models/platform_model.dart';
import '../widgets/button.dart';
@@ -35,12 +37,21 @@ class _ConnectionPageState extends State<ConnectionPage>
Timer? _updateTimer;
final RxBool _idInputFocused = false.obs;
final FocusNode _idFocusNode = FocusNode();
var svcStopped = Get.find<RxBool>(tag: 'stop-service');
var svcIsUsingPublicServer = true.obs;
bool isWindowMinimized = false;
List<Peer> peers = [];
List _frontN<T>(List list, int n) {
if (list.length <= n) {
return list;
} else {
return list.sublist(0, n);
}
}
bool isPeersLoading = false;
bool isPeersLoaded = false;
@override
void initState() {
@@ -58,12 +69,6 @@ class _ConnectionPageState extends State<ConnectionPage>
_updateTimer = periodic_immediate(Duration(seconds: 1), () async {
updateStatus();
});
_idFocusNode.addListener(() {
_idInputFocused.value = _idFocusNode.hasFocus;
// select all to faciliate removing text, just following the behavior of address input of chrome
_idController.selection = TextSelection(
baseOffset: 0, extentOffset: _idController.value.text.length);
});
Get.put<IDTextEditingController>(_idController);
windowManager.addListener(this);
}
@@ -76,6 +81,9 @@ class _ConnectionPageState extends State<ConnectionPage>
if (Get.isRegistered<IDTextEditingController>()) {
Get.delete<IDTextEditingController>();
}
if (Get.isRegistered<TextEditingController>()){
Get.delete<TextEditingController>();
}
super.dispose();
}
@@ -142,8 +150,20 @@ class _ConnectionPageState extends State<ConnectionPage>
connect(context, id, isFileTransfer: isFileTransfer);
}
Future<void> _fetchPeers() async {
setState(() {
isPeersLoading = true;
});
await Future.delayed(Duration(milliseconds: 100));
peers = await getAllPeers();
setState(() {
isPeersLoading = false;
isPeersLoaded = true;
});
}
/// UI for the remote ID TextField.
/// Search for a peer and connect to it if the id exists.
/// Search for a peer.
Widget _buildRemoteIDTextField(BuildContext context) {
var w = Container(
width: 320 + 20 * 2,
@@ -171,36 +191,127 @@ class _ConnectionPageState extends State<ConnectionPage>
Row(
children: [
Expanded(
child: Obx(
() => TextField(
maxLength: 90,
autocorrect: false,
enableSuggestions: false,
keyboardType: TextInputType.visiblePassword,
focusNode: _idFocusNode,
style: const TextStyle(
fontFamily: 'WorkSans',
fontSize: 22,
height: 1.4,
),
maxLines: 1,
cursorColor:
Theme.of(context).textTheme.titleLarge?.color,
decoration: InputDecoration(
filled: false,
counterText: '',
hintText: _idInputFocused.value
? null
: translate('Enter Remote ID'),
contentPadding: const EdgeInsets.symmetric(
horizontal: 15, vertical: 13)),
controller: _idController,
inputFormatters: [IDTextInputFormatter()],
onSubmitted: (s) {
onConnect();
},
),
),
child:
Autocomplete<Peer>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return const Iterable<Peer>.empty();
}
else if (peers.isEmpty && !isPeersLoaded) {
Peer emptyPeer = Peer(
id: '',
username: '',
hostname: '',
alias: '',
platform: '',
tags: [],
hash: '',
forceAlwaysRelay: false,
rdpPort: '',
rdpUsername: '',
loginName: '',
);
return [emptyPeer];
}
else {
String textWithoutSpaces = textEditingValue.text.replaceAll(" ", "");
if (int.tryParse(textWithoutSpaces) != null) {
textEditingValue = TextEditingValue(
text: textWithoutSpaces,
selection: textEditingValue.selection,
);
}
String textToFind = textEditingValue.text.toLowerCase();
return peers.where((peer) =>
peer.id.toLowerCase().contains(textToFind) ||
peer.username.toLowerCase().contains(textToFind) ||
peer.hostname.toLowerCase().contains(textToFind) ||
peer.alias.toLowerCase().contains(textToFind))
.toList();
}
},
fieldViewBuilder: (BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode ,
VoidCallback onFieldSubmitted,
) {
fieldTextEditingController.text = _idController.text;
Get.put<TextEditingController>(fieldTextEditingController);
fieldFocusNode.addListener(() async {
_idInputFocused.value = fieldFocusNode.hasFocus;
if (fieldFocusNode.hasFocus && !isPeersLoading){
_fetchPeers();
}
});
final textLength = fieldTextEditingController.value.text.length;
// select all to facilitate removing text, just following the behavior of address input of chrome
fieldTextEditingController.selection = TextSelection(baseOffset: 0, extentOffset: textLength);
return Obx(() =>
TextField(
maxLength: 90,
autocorrect: false,
enableSuggestions: false,
keyboardType: TextInputType.visiblePassword,
focusNode: fieldFocusNode,
style: const TextStyle(
fontFamily: 'WorkSans',
fontSize: 22,
height: 1.4,
),
maxLines: 1,
cursorColor: Theme.of(context).textTheme.titleLarge?.color,
decoration: InputDecoration(
filled: false,
counterText: '',
hintText: _idInputFocused.value
? null
: translate('Enter Remote ID'),
contentPadding: const EdgeInsets.symmetric(
horizontal: 15, vertical: 13)),
controller: fieldTextEditingController,
inputFormatters: [IDTextInputFormatter()],
onChanged: (v) {
_idController.id = v;
},
));
},
optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<Peer> onSelected, Iterable<Peer> options) {
double maxHeight = options.length * 50;
maxHeight = maxHeight > 200 ? 200 : maxHeight;
return Align(
alignment: Alignment.topLeft,
child: ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Material(
elevation: 4,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: maxHeight,
maxWidth: 319,
),
child: peers.isEmpty && isPeersLoading
? Container(
height: 80,
child: Center(
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
)
: Padding(
padding: const EdgeInsets.only(top: 5),
child: ListView(
children: options.map((peer) => AutocompletePeerTile(idController: _idController, peer: peer)).toList(),
),
),
),
)),
);
},
)
),
],
),

View File

@@ -329,8 +329,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
"Click to download", () async {
final Uri url = Uri.parse('https://rustdesk.com/download');
await launchUrl(url);
},
closeButton: true);
}, closeButton: true);
}
if (systemError.isNotEmpty) {
return buildInstallCard("", systemError, "", () {});
@@ -379,16 +378,39 @@ class _DesktopHomePageState extends State<DesktopHomePage>
// });
// }
} else if (Platform.isLinux) {
final LinuxCards = <Widget>[];
if (bind.isSelinuxEnforcing()) {
// Check is SELinux enforcing, but show user a tip of is SELinux enabled for simple.
final keyShowSelinuxHelpTip = "show-selinux-help-tip";
if (bind.mainGetLocalOption(key: keyShowSelinuxHelpTip) != 'N') {
LinuxCards.add(buildInstallCard(
"Warning", "selinux_tip", "", () async {},
marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
help: 'Help',
link:
'https://rustdesk.com/docs/en/client/linux/#permissions-issue',
closeButton: true,
closeOption: keyShowSelinuxHelpTip,
));
}
}
if (bind.mainCurrentIsWayland()) {
return buildInstallCard(
LinuxCards.add(buildInstallCard(
"Warning", "wayland_experiment_tip", "", () async {},
marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
help: 'Help',
link: 'https://rustdesk.com/docs/en/manual/linux/#x11-required');
link: 'https://rustdesk.com/docs/en/manual/linux/#x11-required'));
} else if (bind.mainIsLoginWayland()) {
return buildInstallCard("Warning",
LinuxCards.add(buildInstallCard("Warning",
"Login screen using Wayland is not supported", "", () async {},
marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
help: 'Help',
link: 'https://rustdesk.com/docs/en/manual/linux/#login-screen');
link: 'https://rustdesk.com/docs/en/manual/linux/#login-screen'));
}
if (LinuxCards.isNotEmpty) {
return Column(
children: LinuxCards,
);
}
}
return Container();
@@ -396,18 +418,26 @@ class _DesktopHomePageState extends State<DesktopHomePage>
Widget buildInstallCard(String title, String content, String btnText,
GestureTapCallback onPressed,
{String? help, String? link, bool? closeButton}) {
void closeCard() {
setState(() {
isCardClosed = true;
});
{double marginTop = 20.0, String? help, String? link, bool? closeButton, String? closeOption}) {
void closeCard() async {
if (closeOption != null) {
await bind.mainSetLocalOption(key: closeOption, value: 'N');
if (bind.mainGetLocalOption(key: closeOption) == 'N') {
setState(() {
isCardClosed = true;
});
}
} else {
setState(() {
isCardClosed = true;
});
}
}
return Stack(
children: [
Container(
margin: EdgeInsets.only(top: 20),
margin: EdgeInsets.only(top: marginTop),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
@@ -555,6 +585,22 @@ class _DesktopHomePageState extends State<DesktopHomePage>
Get.put<RxBool>(svcStopped, tag: 'stop-service');
rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);
screenToMap(window_size.Screen screen) => {
'frame': {
'l': screen.frame.left,
't': screen.frame.top,
'r': screen.frame.right,
'b': screen.frame.bottom,
},
'visibleFrame': {
'l': screen.visibleFrame.left,
't': screen.visibleFrame.top,
'r': screen.visibleFrame.right,
'b': screen.visibleFrame.bottom,
},
'scaleFactor': screen.scaleFactor,
};
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
debugPrint(
"[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
@@ -563,24 +609,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
} else if (call.method == kWindowGetWindowInfo) {
final screen = (await window_size.getWindowInfo()).screen;
if (screen == null) {
return "";
return '';
} else {
return jsonEncode({
'frame': {
'l': screen.frame.left,
't': screen.frame.top,
'r': screen.frame.right,
'b': screen.frame.bottom,
},
'visibleFrame': {
'l': screen.visibleFrame.left,
't': screen.visibleFrame.top,
'r': screen.visibleFrame.right,
'b': screen.visibleFrame.bottom,
},
'scaleFactor': screen.scaleFactor,
});
return jsonEncode(screenToMap(screen));
}
} else if (call.method == kWindowGetScreenList) {
return jsonEncode(
(await window_size.getScreenList()).map(screenToMap).toList());
} else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow();
} else if (call.method == kWindowEventShow) {
@@ -613,8 +648,9 @@ class _DesktopHomePageState extends State<DesktopHomePage>
final peerId = args['peer_id'] as String;
final display = args['display'] as int;
final displayCount = args['display_count'] as int;
final screenRect = parseParamScreenRect(args);
await rustDeskWinManager.openMonitorSession(
windowId, peerId, display, displayCount);
windowId, peerId, display, displayCount, screenRect);
}
});
_uniLinksSubscription = listenUniLinks();

View File

@@ -1324,6 +1324,8 @@ class _DisplayState extends State<_Display> {
if (useTextureRender) {
children.add(otherRow('Show displays as individual windows',
kKeyShowDisplaysAsIndividualWindows));
children.add(otherRow('Use all my displays for the remote session',
kKeyUseAllMyDisplaysForTheRemoteSession));
}
return _Card(title: 'Other Default Options', children: children);
}

View File

@@ -80,7 +80,7 @@ class _RemotePageState extends State<RemotePage>
late RxBool _keyboardEnabled;
final Map<int, RenderTexture> _renderTextures = {};
final _blockableOverlayState = BlockableOverlayState();
var _blockableOverlayState = BlockableOverlayState();
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
@@ -253,9 +253,9 @@ class _RemotePageState extends State<RemotePage>
onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null,
setRemoteState: setState,
);
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: Stack(
bodyWidget() {
return Stack(
children: [
Container(
color: Colors.black,
@@ -281,7 +281,7 @@ class _RemotePageState extends State<RemotePage>
},
inputModel: _ffi.inputModel,
child: getBodyForDesktop(context))),
Obx(() => Stack(
Stack(
children: [
_ffi.ffiModel.pi.isSet.isTrue &&
_ffi.ffiModel.waitForFirstImage.isTrue
@@ -298,9 +298,34 @@ class _RemotePageState extends State<RemotePage>
: remoteToolbar(context),
_ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
],
)),
),
],
),
);
}
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: Obx(() {
final imageReady = _ffi.ffiModel.pi.isSet.isTrue &&
_ffi.ffiModel.waitForFirstImage.isFalse;
if (imageReady) {
// `dismissAll()` is to ensure that the state is clean.
// It's ok to call dismissAll() here.
_ffi.dialogManager.dismissAll();
// Recreate the block state to refresh the state.
_blockableOverlayState = BlockableOverlayState();
_blockableOverlayState.applyFfi(_ffi);
// Block the whole `bodyWidget()` when dialog shows.
return BlockableOverlay(
underlying: bodyWidget(),
state: _blockableOverlayState,
);
} else {
// `_blockableOverlayState` is not recreated here.
// The toolbar's block state won't work properly when reconnecting, but that's okay.
return bodyWidget();
}
}),
);
}
@@ -677,7 +702,8 @@ class _ImagePaintState extends State<ImagePaint> {
} else {
final key = cache.updateGetKey(scale);
if (!cursor.cachedKeys.contains(key)) {
debugPrint("Register custom cursor with key $key (${cache.hotx},${cache.hoty})");
debugPrint(
"Register custom cursor with key $key (${cache.hotx},${cache.hoty})");
// [Safety]
// It's ok to call async registerCursor in current synchronous context,
// because activating the cursor is also an async call and will always

View File

@@ -48,6 +48,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
late ToolbarState _toolbarState;
String? peerId;
bool _isScreenRectSet = false;
int? _display;
var connectionMap = RxList<Widget>.empty(growable: true);
@@ -59,6 +61,10 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final tabWindowId = params['tab_window_id'];
final display = params['display'];
final displays = params['displays'];
final screenRect = parseParamScreenRect(params);
_isScreenRectSet = screenRect != null;
_display = display as int?;
tryMoveToScreenAndSetFullscreen(screenRect);
if (peerId != null) {
ConnectionTypeState.init(peerId!);
tabController.onSelected = (id) {
@@ -115,11 +121,16 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final tabWindowId = args['tab_window_id'];
final display = args['display'];
final displays = args['displays'];
final screenRect = parseParamScreenRect(args);
windowOnTop(windowId());
tryMoveToScreenAndSetFullscreen(screenRect);
if (tabController.length == 0) {
if (Platform.isMacOS && stateGlobal.closeOnFullscreen) {
// Show the hidden window.
if (Platform.isMacOS && stateGlobal.closeOnFullscreen == true) {
stateGlobal.setFullscreen(true);
}
// Reset the state
stateGlobal.closeOnFullscreen = null;
}
ConnectionTypeState.init(id);
_toolbarState.setShow(
@@ -196,15 +207,18 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
_update_remote_count();
return returnValue;
});
Future.delayed(Duration.zero, () {
restoreWindowPosition(
WindowType.RemoteDesktop,
windowId: windowId(),
peerId: tabController.state.value.tabs.isEmpty
? null
: tabController.state.value.tabs[0].key,
);
});
if (!_isScreenRectSet) {
Future.delayed(Duration.zero, () {
restoreWindowPosition(
WindowType.RemoteDesktop,
windowId: windowId(),
peerId: tabController.state.value.tabs.isEmpty
? null
: tabController.state.value.tabs[0].key,
display: _display,
);
});
}
}
@override
@@ -451,6 +465,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
c++;
}
}
loopCloseWindow();
}
ConnectionTypeState.delete(id);

View File

@@ -1,12 +1,10 @@
import 'dart:convert';
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/toolbar.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
@@ -22,17 +20,12 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:window_size/window_size.dart' as window_size;
import '../../common.dart';
import '../../common/widgets/dialog.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import './popup_menu.dart';
import './kb_layout_type_chooser.dart';
const _kKeyLegacyMode = 'legacy';
const _kKeyMapMode = 'map';
const _kKeyTranslateMode = 'translate';
class ToolbarState {
final kStoreKey = 'remoteMenubarState';
late RxBool show;
@@ -353,10 +346,10 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
int get windowId => stateGlobal.windowId;
bool get isFullscreen => stateGlobal.fullscreen;
void _setFullscreen(bool v) {
stateGlobal.setFullscreen(v);
setState(() {});
// stateGlobal.fullscreen is RxBool now, no need to call setState.
// setState(() {});
}
RxBool get show => widget.state.show;
@@ -480,7 +473,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
}
toolbarItems.add(_RecordMenu(ffi: widget.ffi));
toolbarItems.add(_RecordMenu());
toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi));
return Column(
mainAxisSize: MainAxisSize.min,
@@ -744,42 +737,14 @@ class _MonitorMenu extends StatelessWidget {
);
}
// Open new tab or window to show this monitor.
// For now just open new window.
openMonitorInNewTabOrWindow(int i, PeerInfo pi) {
if (kWindowId == null) {
// unreachable
debugPrint('openMonitorInNewTabOrWindow, unreachable! kWindowId is null');
return;
}
DesktopMultiWindow.invokeMethod(
kMainWindowId,
kWindowEventOpenMonitorSession,
jsonEncode({
'window_id': kWindowId!,
'peer_id': ffi.id,
'display': i,
'display_count': pi.displays.length,
}));
}
openMonitorInTheSameTab(int i, PeerInfo pi) {
final displays = i == kAllDisplayValue
? List.generate(pi.displays.length, (index) => index)
: [i];
bind.sessionSwitchDisplay(
sessionId: ffi.sessionId, value: Int32List.fromList(displays));
ffi.ffiModel.switchToNewDisplay(i, ffi.sessionId, id);
}
onPressed(int i, PeerInfo pi) {
_menuDismissCallback(ffi);
RxInt display = CurrentDisplayState.find(id);
if (display.value != i) {
if (isChooseDisplayToOpenInNewWindow(pi, ffi.sessionId)) {
openMonitorInNewTabOrWindow(i, pi);
openMonitorInNewTabOrWindow(i, ffi.id, pi);
} else {
openMonitorInTheSameTab(i, pi);
openMonitorInTheSameTab(i, ffi, pi);
}
}
}
@@ -827,7 +792,7 @@ class ScreenAdjustor {
required this.cbExitFullscreen,
});
bool get isFullscreen => stateGlobal.fullscreen;
bool get isFullscreen => stateGlobal.fullscreen.isTrue;
int get windowId => stateGlobal.windowId;
adjustWindow(BuildContext context) {
@@ -981,7 +946,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
cbExitFullscreen: () => widget.setFullscreen(false),
);
bool get isFullscreen => stateGlobal.fullscreen;
int get windowId => stateGlobal.windowId;
Map<String, bool> get perms => widget.ffi.ffiModel.permissions;
PeerInfo get pi => widget.ffi.ffiModel.pi;
@@ -1438,18 +1402,16 @@ class _KeyboardMenu extends StatelessWidget {
Widget build(BuildContext context) {
var ffiModel = Provider.of<FfiModel>(context);
if (!ffiModel.keyboard) return Offstage();
// If use flutter to grab keys, we can only use one mode.
// Map mode and Legacy mode, at least one of them is supported.
String? modeOnly;
if (stateGlobal.grabKeyboard) {
if (bind.sessionIsKeyboardModeSupported(
sessionId: ffi.sessionId, mode: _kKeyMapMode)) {
bind.sessionSetKeyboardMode(
sessionId: ffi.sessionId, value: _kKeyMapMode);
modeOnly = _kKeyMapMode;
sessionId: ffi.sessionId, mode: kKeyMapMode)) {
modeOnly = kKeyMapMode;
} else if (bind.sessionIsKeyboardModeSupported(
sessionId: ffi.sessionId, mode: _kKeyLegacyMode)) {
bind.sessionSetKeyboardMode(
sessionId: ffi.sessionId, value: _kKeyLegacyMode);
modeOnly = _kKeyLegacyMode;
sessionId: ffi.sessionId, mode: kKeyLegacyMode)) {
modeOnly = kKeyLegacyMode;
}
}
return _IconSubmenuButton(
@@ -1471,13 +1433,13 @@ class _KeyboardMenu extends StatelessWidget {
keyboardMode(String? modeOnly) {
return futureBuilder(future: () async {
return await bind.sessionGetKeyboardMode(sessionId: ffi.sessionId) ??
_kKeyLegacyMode;
kKeyLegacyMode;
}(), hasData: (data) {
final groupValue = data as String;
List<InputModeMenu> modes = [
InputModeMenu(key: _kKeyLegacyMode, menu: 'Legacy mode'),
InputModeMenu(key: _kKeyMapMode, menu: 'Map mode'),
InputModeMenu(key: _kKeyTranslateMode, menu: 'Translate mode'),
InputModeMenu(key: kKeyLegacyMode, menu: 'Legacy mode'),
InputModeMenu(key: kKeyMapMode, menu: 'Map mode'),
InputModeMenu(key: kKeyTranslateMode, menu: 'Translate mode'),
];
List<RdoMenuButton> list = [];
final enabled = !ffi.ffiModel.viewOnly;
@@ -1495,12 +1457,12 @@ class _KeyboardMenu extends StatelessWidget {
continue;
}
if (pi.isWayland && mode.key != _kKeyMapMode) {
if (pi.isWayland && mode.key != kKeyMapMode) {
continue;
}
var text = translate(mode.menu);
if (mode.key == _kKeyTranslateMode) {
if (mode.key == kKeyTranslateMode) {
text = '$text beta';
}
list.add(RdoMenuButton<String>(
@@ -1677,17 +1639,17 @@ class _VoiceCallMenu extends StatelessWidget {
}
class _RecordMenu extends StatelessWidget {
final FFI ffi;
const _RecordMenu({Key? key, required this.ffi}) : super(key: key);
const _RecordMenu({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
var ffiModel = Provider.of<FfiModel>(context);
var ffi = Provider.of<FfiModel>(context);
var recordingModel = Provider.of<RecordingModel>(context);
final visible =
recordingModel.start || ffiModel.permissions['recording'] != false;
(recordingModel.start || ffi.permissions['recording'] != false) &&
ffi.pi.currentDisplay != kAllDisplayValue;
if (!visible) return Offstage();
final menuButton = _IconMenuButton(
return _IconMenuButton(
assetName: 'assets/rec.svg',
tooltip: recordingModel.start
? 'Stop session recording'
@@ -1700,14 +1662,6 @@ class _RecordMenu extends StatelessWidget {
? _ToolbarTheme.hoverRedColor
: _ToolbarTheme.hoverBlueColor,
);
return ChangeNotifierProvider.value(
value: ffi.qualityMonitorModel,
child: Consumer<QualityMonitorModel>(
builder: (context, model, child) => Offstage(
// If already started, AV1->Hidden/Stop, Other->Start, same as actual
offstage: model.data.codecFormat == 'AV1',
child: menuButton,
)));
}
}
@@ -1722,7 +1676,7 @@ class _CloseMenu extends StatelessWidget {
return _IconMenuButton(
assetName: 'assets/close.svg',
tooltip: 'Close',
onPressed: () => clientClose(ffi.sessionId, ffi.dialogManager),
onPressed: () => closeConnection(id: id),
color: _ToolbarTheme.redColor,
hoverColor: _ToolbarTheme.hoverRedColor,
);
@@ -2090,32 +2044,34 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
mainAxisSize: MainAxisSize.min,
children: [
_buildDraggable(context),
TextButton(
onPressed: () {
widget.setFullscreen(!isFullscreen);
setState(() {});
},
child: Tooltip(
message: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'),
child: Icon(
isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen,
size: iconSize,
),
),
),
Offstage(
offstage: !isFullscreen,
child: TextButton(
onPressed: () => widget.setMinimize(),
child: Tooltip(
message: translate('Minimize'),
child: Icon(
Icons.remove,
size: iconSize,
Obx(() => TextButton(
onPressed: () {
widget.setFullscreen(!isFullscreen.value);
},
child: Tooltip(
message: translate(
isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'),
child: Icon(
isFullscreen.isTrue
? Icons.fullscreen_exit
: Icons.fullscreen,
size: iconSize,
),
),
),
),
),
)),
Obx(() => Offstage(
offstage: isFullscreen.isFalse,
child: TextButton(
onPressed: () => widget.setMinimize(),
child: Tooltip(
message: translate('Minimize'),
child: Icon(
Icons.remove,
size: iconSize,
),
),
),
)),
TextButton(
onPressed: () => setState(() {
widget.show.value = !widget.show.value;

View File

@@ -448,6 +448,7 @@ class DesktopTab extends StatelessWidget {
isMainWindow: isMainWindow,
tabType: tabType,
state: state,
tabController: controller,
tail: tail,
showMinimize: showMinimize,
showMaximize: showMaximize,
@@ -463,6 +464,7 @@ class WindowActionPanel extends StatefulWidget {
final bool isMainWindow;
final DesktopTabType tabType;
final Rx<DesktopTabState> state;
final DesktopTabController tabController;
final bool showMinimize;
final bool showMaximize;
@@ -475,6 +477,7 @@ class WindowActionPanel extends StatefulWidget {
required this.isMainWindow,
required this.tabType,
required this.state,
required this.tabController,
this.tail,
this.showMinimize = true,
this.showMaximize = true,
@@ -580,19 +583,38 @@ class WindowActionPanelState extends State<WindowActionPanel>
void onWindowClose() async {
mainWindowClose() async => await windowManager.hide();
notMainWindowClose(WindowController controller) async {
await controller.hide();
await Future.wait([
rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}),
widget.onClose?.call() ?? Future.microtask(() => null)
]);
if (widget.tabController.length == 0) {
debugPrint("close emtpy multiwindow, hide");
await controller.hide();
await rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
} else {
debugPrint("close not emtpy multiwindow from taskbar");
if (Platform.isWindows) {
await controller.show();
await controller.focus();
final res = await widget.onClose?.call() ?? true;
if (res) {
Future.delayed(Duration.zero, () async {
// onWindowClose will be called again to hide
await WindowController.fromWindowId(kWindowId!).close();
});
}
} else {
// ubuntu22.04 windowOnTop not work from taskbar
widget.tabController.clear();
Future.delayed(Duration.zero, () async {
// onWindowClose will be called again to hide
await WindowController.fromWindowId(kWindowId!).close();
});
}
}
}
macOSWindowClose(
Future<void> Function() restoreFunc,
Future<bool> Function() checkFullscreen,
Future<void> Function() closeFunc) async {
await restoreFunc();
Future<bool> Function() checkFullscreen,
Future<void> Function() closeFunc,
) async {
_macOSCheckRestoreCounter = 0;
_macOSCheckRestoreTimer =
Timer.periodic(Duration(milliseconds: 30), (timer) async {
@@ -612,26 +634,38 @@ class WindowActionPanelState extends State<WindowActionPanel>
}
// macOS specific workaround, the window is not hiding when in fullscreen.
if (Platform.isMacOS && await windowManager.isFullScreen()) {
stateGlobal.closeOnFullscreen = true;
stateGlobal.closeOnFullscreen ??= true;
await windowManager.setFullScreen(false);
await macOSWindowClose(
() async => await windowManager.setFullScreen(false),
() async => await windowManager.isFullScreen(),
mainWindowClose);
() async => await windowManager.isFullScreen(),
mainWindowClose,
);
} else {
stateGlobal.closeOnFullscreen = false;
stateGlobal.closeOnFullscreen ??= false;
await mainWindowClose();
}
} else {
// it's safe to hide the subwindow
final controller = WindowController.fromWindowId(kWindowId!);
if (Platform.isMacOS && await controller.isFullScreen()) {
stateGlobal.closeOnFullscreen = true;
await macOSWindowClose(
() async => await controller.setFullscreen(false),
() async => await controller.isFullScreen(),
() async => await notMainWindowClose(controller));
if (Platform.isMacOS) {
// onWindowClose() maybe called multiple times because of loopCloseWindow() in remote_tab_page.dart.
// use ??= to make sure the value is set on first call.
if (await widget.onClose?.call() ?? true) {
if (await controller.isFullScreen()) {
stateGlobal.closeOnFullscreen ??= true;
await controller.setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false);
await macOSWindowClose(
() async => await controller.isFullScreen(),
() async => await notMainWindowClose(controller),
);
} else {
stateGlobal.closeOnFullscreen ??= false;
await notMainWindowClose(controller);
}
}
} else {
stateGlobal.closeOnFullscreen = false;
await notMainWindowClose(controller);
}
}

View File

@@ -198,8 +198,16 @@ void runMultiWindow(
}
switch (appType) {
case kAppTypeDesktopRemote:
await restoreWindowPosition(WindowType.RemoteDesktop,
windowId: kWindowId!, peerId: argument['id'] as String?);
// If screen rect is set, the window will be moved to the target screen and then set fullscreen.
if (argument['screen_rect'] == null) {
// display can be used to control the offset of the window.
await restoreWindowPosition(
WindowType.RemoteDesktop,
windowId: kWindowId!,
peerId: argument['id'] as String?,
display: argument['display'] as int?,
);
}
break;
case kAppTypeDesktopFileTransfer:
await restoreWindowPosition(WindowType.FileTransfer,

View File

@@ -6,10 +6,12 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import '../../common.dart';
import '../../common/widgets/login.dart';
import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/autocomplete.dart';
import '../../consts.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
@@ -42,6 +44,16 @@ class _ConnectionPageState extends State<ConnectionPage> {
/// Update url. If it's not null, means an update is available.
var _updateUrl = '';
List<Peer> peers = [];
List _frontN<T>(List list, int n) {
if (list.length <= n) {
return list;
} else {
return list.sublist(0, n);
}
}
bool isPeersLoading = false;
bool isPeersLoaded = false;
@override
void initState() {
@@ -116,6 +128,18 @@ class _ConnectionPageState extends State<ConnectionPage> {
color: Colors.white, fontWeight: FontWeight.bold))));
}
Future<void> _fetchPeers() async {
setState(() {
isPeersLoading = true;
});
await Future.delayed(Duration(milliseconds: 100));
peers = await getAllPeers();
setState(() {
isPeersLoading = false;
isPeersLoaded = true;
});
}
/// UI for the remote ID TextField.
/// Search for a peer and connect to it if the id exists.
Widget _buildRemoteIDTextField() {
@@ -133,12 +157,69 @@ class _ConnectionPageState extends State<ConnectionPage> {
Expanded(
child: Container(
padding: const EdgeInsets.only(left: 16, right: 16),
child: AutoSizeTextField(
child: Autocomplete<Peer>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return const Iterable<Peer>.empty();
}
else if (peers.isEmpty && !isPeersLoaded) {
Peer emptyPeer = Peer(
id: '',
username: '',
hostname: '',
alias: '',
platform: '',
tags: [],
hash: '',
forceAlwaysRelay: false,
rdpPort: '',
rdpUsername: '',
loginName: '',
);
return [emptyPeer];
}
else {
String textWithoutSpaces = textEditingValue.text.replaceAll(" ", "");
if (int.tryParse(textWithoutSpaces) != null) {
textEditingValue = TextEditingValue(
text: textWithoutSpaces,
selection: textEditingValue.selection,
);
}
String textToFind = textEditingValue.text.toLowerCase();
return peers.where((peer) =>
peer.id.toLowerCase().contains(textToFind) ||
peer.username.toLowerCase().contains(textToFind) ||
peer.hostname.toLowerCase().contains(textToFind) ||
peer.alias.toLowerCase().contains(textToFind))
.toList();
}
},
fieldViewBuilder: (BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode, VoidCallback onFieldSubmitted) {
fieldTextEditingController.text = _idController.text;
fieldFocusNode.addListener(() async{
_idEmpty.value = fieldTextEditingController.text.isEmpty;
if (fieldFocusNode.hasFocus && !isPeersLoading){
_fetchPeers();
}
});
final textLength = fieldTextEditingController.value.text.length;
// select all to facilitate removing text, just following the behavior of address input of chrome
fieldTextEditingController.selection = TextSelection(baseOffset: 0, extentOffset: textLength);
return AutoSizeTextField(
controller: fieldTextEditingController,
focusNode: fieldFocusNode,
minFontSize: 18,
autocorrect: false,
enableSuggestions: false,
keyboardType: TextInputType.visiblePassword,
// keyboardType: TextInputType.number,
onChanged: (String text) {
_idController.id = text;
},
style: const TextStyle(
fontFamily: 'WorkSans',
fontWeight: FontWeight.bold,
@@ -161,8 +242,36 @@ class _ConnectionPageState extends State<ConnectionPage> {
color: MyTheme.darkGray,
),
),
controller: _idController,
inputFormatters: [IDTextInputFormatter()],
);
},
optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<Peer> onSelected, Iterable<Peer> options) {
double maxHeight = options.length * 50;
maxHeight = maxHeight > 200 ? 200 : maxHeight;
return Align(
alignment: Alignment.topLeft,
child: ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Material(
elevation: 4,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: maxHeight,
maxWidth: 320,
),
child: peers.isEmpty && isPeersLoading
? Container(
height: 80,
child: Center(
child: CircularProgressIndicator(
strokeWidth: 2,
)))
: ListView(
padding: EdgeInsets.only(top: 5),
children: options.map((peer) => AutocompletePeerTile(idController: _idController, peer: peer)).toList(),
))))
);
},
),
),
),
@@ -170,7 +279,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
offstage: _idEmpty.value,
child: IconButton(
onPressed: () {
_idController.clear();
setState(() {
_idController.clear();
});
},
icon: Icon(Icons.clear, color: MyTheme.darkGray)),
)),

View File

@@ -235,7 +235,7 @@ class _RemotePageState extends State<RemotePage> {
clientClose(sessionId, gFFI.dialogManager);
return false;
},
child: getRawPointerAndKeyBody(Scaffold(
child: Scaffold(
// workaround for https://github.com/rustdesk/rustdesk/issues/3131
floatingActionButtonLocation: keyboardIsVisible
? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35)
@@ -281,7 +281,7 @@ class _RemotePageState extends State<RemotePage> {
: Offstage(),
],
)),
body: Overlay(
body: getRawPointerAndKeyBody(Overlay(
initialEntries: [
OverlayEntry(builder: (context) {
return Container(
@@ -763,7 +763,9 @@ void showOptions(
children.add(InkWell(
onTap: () {
if (i == cur) return;
bind.sessionSwitchDisplay(sessionId: gFFI.sessionId, value: Int32List.fromList([i]));
gFFI.recordingModel.onClose();
bind.sessionSwitchDisplay(
sessionId: gFFI.sessionId, value: Int32List.fromList([i]));
gFFI.dialogManager.dismissAll();
},
child: Ink(

View File

@@ -103,7 +103,7 @@ class ChatModel with ChangeNotifier {
void setOverlayState(BlockableOverlayState blockableOverlayState) {
_blockableOverlayState = blockableOverlayState;
_blockableOverlayState!.addMiddleBlockedListener((v) {
_blockableOverlayState.addMiddleBlockedListener((v) {
if (!v) {
isWindowFocus.value = false;
if (isWindowFocus.value) {
@@ -197,9 +197,9 @@ class ChatModel with ChangeNotifier {
showChatWindowOverlay({Offset? chatInitPos}) {
if (chatWindowOverlayEntry != null) return;
isWindowFocus.value = true;
_blockableOverlayState?.setMiddleBlocked(true);
_blockableOverlayState.setMiddleBlocked(true);
final overlayState = _blockableOverlayState?.state;
final overlayState = _blockableOverlayState.state;
if (overlayState == null) return;
if (isMobile &&
!gFFI.chatModel.currentKey.isOut && // not in remote page
@@ -212,7 +212,7 @@ class ChatModel with ChangeNotifier {
onPointerDown: (_) {
if (!isWindowFocus.value) {
isWindowFocus.value = true;
_blockableOverlayState?.setMiddleBlocked(true);
_blockableOverlayState.setMiddleBlocked(true);
}
},
child: DraggableChatWindow(
@@ -228,7 +228,7 @@ class ChatModel with ChangeNotifier {
hideChatWindowOverlay() {
if (chatWindowOverlayEntry != null) {
_blockableOverlayState?.setMiddleBlocked(false);
_blockableOverlayState.setMiddleBlocked(false);
chatWindowOverlayEntry!.remove();
chatWindowOverlayEntry = null;
return;

View File

@@ -261,6 +261,7 @@ class FileController {
required this.getOtherSideDirectoryData});
String get homePath => options.value.home;
void set homePath(String path) => options.value.home = path;
OverlayDialogManager? get dialogManager => rootState.target?.dialogManager;
String get shortPath {
@@ -376,6 +377,11 @@ class FileController {
}
void goToHomeDirectory() {
if (isLocal) {
openDirectory(homePath);
return;
}
homePath = "";
openDirectory(homePath);
}

View File

@@ -227,7 +227,7 @@ class FfiModel with ChangeNotifier {
}, sessionId, peerId);
updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId);
setConnectionType(peerId, data.secure, data.direct);
await handlePeerInfo(data.peerInfo, peerId);
await handlePeerInfo(data.peerInfo, peerId, true);
for (final element in data.cursorDataList) {
updateLastCursorId(element);
await handleCursorData(element);
@@ -245,7 +245,7 @@ class FfiModel with ChangeNotifier {
if (name == 'msgbox') {
handleMsgBox(evt, sessionId, peerId);
} else if (name == 'peer_info') {
handlePeerInfo(evt, peerId);
handlePeerInfo(evt, peerId, false);
} else if (name == 'sync_peer_info') {
handleSyncPeerInfo(evt, sessionId, peerId);
} else if (name == 'connection_ready') {
@@ -430,14 +430,12 @@ class FfiModel with ChangeNotifier {
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
final curDisplay = int.parse(evt['display']);
// The message should be handled by the another UI session.
if (isChooseDisplayToOpenInNewWindow(_pi, sessionId)) {
if (curDisplay != _pi.currentDisplay) {
return;
}
}
if (_pi.currentDisplay != kAllDisplayValue) {
if (bind.peerGetDefaultSessionsCount(id: peerId) > 1) {
if (curDisplay != _pi.currentDisplay) {
return;
}
}
_pi.currentDisplay = curDisplay;
}
@@ -514,7 +512,9 @@ class FfiModel with ChangeNotifier {
String link, bool hasRetry, OverlayDialogManager dialogManager,
{bool? hasCancel}) {
msgBox(sessionId, type, title, text, link, dialogManager,
hasCancel: hasCancel, reconnect: reconnect);
hasCancel: hasCancel,
reconnect: reconnect,
reconnectTimeout: hasRetry ? _reconnects : null);
_timer?.cancel();
if (hasRetry) {
_timer = Timer(Duration(seconds: _reconnects), () {
@@ -530,6 +530,7 @@ class FfiModel with ChangeNotifier {
bool forceRelay) {
bind.sessionReconnect(sessionId: sessionId, forceRelay: forceRelay);
clearPermissions();
dialogManager.dismissAll();
dialogManager.showLoading(translate('Connecting...'),
onCancel: closeConnection);
}
@@ -623,7 +624,7 @@ class FfiModel with ChangeNotifier {
}
/// Handle the peer info event based on [evt].
handlePeerInfo(Map<String, dynamic> evt, String peerId) async {
handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
cachedPeerData.peerInfo = evt;
// recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
@@ -689,12 +690,12 @@ class FfiModel with ChangeNotifier {
sessionId: sessionId, arg: 'view-only'));
}
if (connType == ConnType.defaultConn) {
final platformDdditions = evt['platform_additions'];
if (platformDdditions != null && platformDdditions != '') {
final platformAdditions = evt['platform_additions'];
if (platformAdditions != null && platformAdditions != '') {
try {
_pi.platformDdditions = json.decode(platformDdditions);
_pi.platformAdditions = json.decode(platformAdditions);
} catch (e) {
debugPrint('Failed to decode platformDdditions $e');
debugPrint('Failed to decode platformAdditions $e');
}
}
}
@@ -702,7 +703,86 @@ class FfiModel with ChangeNotifier {
_pi.isSet.value = true;
stateGlobal.resetLastResolutionGroupValues(peerId);
if (isDesktop) {
checkDesktopKeyboardMode();
}
notifyListeners();
if (!isCache) {
tryUseAllMyDisplaysForTheRemoteSession(peerId);
}
}
checkDesktopKeyboardMode() async {
final curMode = await bind.sessionGetKeyboardMode(sessionId: sessionId);
if (curMode != null) {
if (bind.sessionIsKeyboardModeSupported(
sessionId: sessionId, mode: curMode)) {
return;
}
}
// If current keyboard mode is not supported, change to another one.
if (stateGlobal.grabKeyboard) {
for (final mode in [kKeyMapMode, kKeyLegacyMode]) {
if (bind.sessionIsKeyboardModeSupported(
sessionId: sessionId, mode: mode)) {
bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode);
break;
}
}
} else {
for (final mode in [kKeyMapMode, kKeyTranslateMode, kKeyLegacyMode]) {
if (bind.sessionIsKeyboardModeSupported(
sessionId: sessionId, mode: mode)) {
bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode);
break;
}
}
}
}
tryUseAllMyDisplaysForTheRemoteSession(String peerId) async {
if (bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
sessionId: sessionId) !=
'Y') {
return;
}
if (!_pi.isSupportMultiDisplay || _pi.displays.length <= 1) {
return;
}
final screenRectList = await getScreenRectList();
if (screenRectList.length <= 1) {
return;
}
// to-do: peer currentDisplay is the primary display, but the primary display may not be the first display.
// local primary display also may not be the first display.
//
// 0 is assumed to be the primary display here, for now.
// move to the first display and set fullscreen
bind.sessionSwitchDisplay(
sessionId: sessionId, value: Int32List.fromList([0]));
_pi.currentDisplay = 0;
try {
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
} catch (e) {
//
}
await tryMoveToScreenAndSetFullscreen(screenRectList[0]);
final length = _pi.displays.length < screenRectList.length
? _pi.displays.length
: screenRectList.length;
for (var i = 1; i < length; i++) {
openMonitorInNewTabOrWindow(i, peerId, _pi,
screenRect: screenRectList[i]);
}
}
tryShowAndroidActionsOverlay({int delayMSecs = 10}) {
@@ -780,6 +860,7 @@ class FfiModel with ChangeNotifier {
}
_pi.displays = newDisplays;
_pi.displaysCount.value = _pi.displays.length;
if (_pi.currentDisplay == kAllDisplayValue) {
updateCurDisplay(sessionId);
// to-do: What if the displays are changed?
@@ -816,6 +897,8 @@ class FfiModel with ChangeNotifier {
// Directly switch to the new display without waiting for the response.
switchToNewDisplay(int display, SessionID sessionId, String peerId) {
// VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays.
parent.target?.recordingModel.onClose();
// no need to wait for the response
pi.currentDisplay = display;
updateCurDisplay(sessionId);
@@ -824,7 +907,6 @@ class FfiModel with ChangeNotifier {
} catch (e) {
//
}
parent.target?.recordingModel.onSwitchDisplay();
}
updateBlockInputState(Map<String, dynamic> evt, String peerId) {
@@ -1806,57 +1888,67 @@ class RecordingModel with ChangeNotifier {
int? width = parent.target?.canvasModel.getDisplayWidth();
int? height = parent.target?.canvasModel.getDisplayHeight();
if (sessionId == null || width == null || height == null) return;
final currentDisplay = parent.target?.ffiModel.pi.currentDisplay;
if (currentDisplay != kAllDisplayValue) {
bind.sessionRecordScreen(
sessionId: sessionId,
start: true,
display: currentDisplay!,
width: width,
height: height);
}
final pi = parent.target?.ffiModel.pi;
if (pi == null) return;
final currentDisplay = pi.currentDisplay;
if (currentDisplay == kAllDisplayValue) return;
bind.sessionRecordScreen(
sessionId: sessionId,
start: true,
display: currentDisplay,
width: width,
height: height);
}
toggle() async {
if (isIOS) return;
final sessionId = parent.target?.sessionId;
if (sessionId == null) return;
final pi = parent.target?.ffiModel.pi;
if (pi == null) return;
final currentDisplay = pi.currentDisplay;
if (currentDisplay == kAllDisplayValue) return;
_start = !_start;
notifyListeners();
await bind.sessionRecordStatus(sessionId: sessionId, status: _start);
await _sendStatusMessage(sessionId, pi, _start);
if (_start) {
final pi = parent.target?.ffiModel.pi;
if (pi != null) {
sessionRefreshVideo(sessionId, pi);
sessionRefreshVideo(sessionId, pi);
if (versionCmp(pi.version, '1.2.4') >= 0) {
// will not receive SwitchDisplay since 1.2.4
onSwitchDisplay();
}
} else {
final currentDisplay = parent.target?.ffiModel.pi.currentDisplay;
if (currentDisplay != kAllDisplayValue) {
bind.sessionRecordScreen(
sessionId: sessionId,
start: false,
display: currentDisplay!,
width: 0,
height: 0);
}
}
}
onClose() {
if (isIOS) return;
final sessionId = parent.target?.sessionId;
if (sessionId == null) return;
_start = false;
final currentDisplay = parent.target?.ffiModel.pi.currentDisplay;
if (currentDisplay != kAllDisplayValue) {
bind.sessionRecordScreen(
sessionId: sessionId,
start: false,
display: currentDisplay!,
display: currentDisplay,
width: 0,
height: 0);
}
}
onClose() async {
if (isIOS) return;
final sessionId = parent.target?.sessionId;
if (sessionId == null) return;
if (!_start) return;
_start = false;
final pi = parent.target?.ffiModel.pi;
if (pi == null) return;
final currentDisplay = pi.currentDisplay;
if (currentDisplay == kAllDisplayValue) return;
await _sendStatusMessage(sessionId, pi, false);
bind.sessionRecordScreen(
sessionId: sessionId,
start: false,
display: currentDisplay,
width: 0,
height: 0);
}
_sendStatusMessage(SessionID sessionId, PeerInfo pi, bool status) async {
await bind.sessionRecordStatus(sessionId: sessionId, status: status);
}
}
class ElevationModel with ChangeNotifier {
@@ -2203,13 +2295,13 @@ class PeerInfo with ChangeNotifier {
List<Display> displays = [];
Features features = Features();
List<Resolution> resolutions = [];
Map<String, dynamic> platformDdditions = {};
Map<String, dynamic> platformAdditions = {};
RxInt displaysCount = 0.obs;
RxBool isSet = false.obs;
bool get isWayland => platformDdditions['is_wayland'] == true;
bool get isHeadless => platformDdditions['headless'] == true;
bool get isWayland => platformAdditions['is_wayland'] == true;
bool get isHeadless => platformAdditions['headless'] == true;
bool get isSupportMultiDisplay => isDesktop && isSupportMultiUiSession;
@@ -2237,7 +2329,7 @@ class PeerInfo with ChangeNotifier {
if (currentDisplay == kAllDisplayValue) {
return null;
}
if (currentDisplay > 0 && currentDisplay < displays.length) {
if (currentDisplay >= 0 && currentDisplay < displays.length) {
return displays[currentDisplay];
} else {
return null;

View File

@@ -11,7 +11,7 @@ enum SvcStatus { notReady, connecting, ready }
class StateGlobal {
int _windowId = -1;
bool grabKeyboard = false;
bool _fullscreen = false;
final RxBool _fullscreen = false.obs;
bool _isMinimized = false;
final RxBool isMaximized = false.obs;
final RxBool _showTabBar = true.obs;
@@ -20,15 +20,15 @@ class StateGlobal {
final RxBool showRemoteToolBar = false.obs;
final svcStatus = SvcStatus.notReady.obs;
// Only used for macOS
bool closeOnFullscreen = false;
bool? closeOnFullscreen;
// Use for desktop -> remote toolbar -> resolution
final Map<String, Map<int, String?>> _lastResolutionGroupValues = {};
int get windowId => _windowId;
bool get fullscreen => _fullscreen;
RxBool get fullscreen => _fullscreen;
bool get isMinimized => _isMinimized;
double get tabBarHeight => fullscreen ? 0 : kDesktopRemoteTabBarHeight;
double get tabBarHeight => fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight;
RxBool get showTabBar => _showTabBar;
RxDouble get resizeEdgeSize => _resizeEdgeSize;
RxDouble get windowBorderWidth => _windowBorderWidth;
@@ -51,7 +51,7 @@ class StateGlobal {
setWindowId(int id) => _windowId = id;
setMaximized(bool v) {
if (!_fullscreen) {
if (!_fullscreen.isTrue) {
if (isMaximized.value != v) {
isMaximized.value = v;
_resizeEdgeSize.value =
@@ -66,29 +66,27 @@ class StateGlobal {
setMinimized(bool v) => _isMinimized = v;
setFullscreen(bool v, {bool procWnd = true}) {
if (_fullscreen != v) {
_fullscreen = v;
_showTabBar.value = !_fullscreen;
_resizeEdgeSize.value = fullscreen
if (_fullscreen.value != v) {
_fullscreen.value = v;
_showTabBar.value = !_fullscreen.value;
_resizeEdgeSize.value = fullscreen.isTrue
? kFullScreenEdgeSize
: isMaximized.isTrue
? kMaximizeEdgeSize
: kWindowEdgeSize;
print(
"fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}");
_windowBorderWidth.value = fullscreen ? 0 : kWindowBorderWidth;
_windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth;
if (procWnd) {
WindowController.fromWindowId(windowId)
.setFullscreen(_fullscreen)
.then((_) {
final wc = WindowController.fromWindowId(windowId);
wc.setFullscreen(_fullscreen.isTrue).then((_) {
// https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982
if (Platform.isWindows && !v) {
Future.delayed(Duration.zero, () async {
final frame =
await WindowController.fromWindowId(windowId).getFrame();
final frame = await wc.getFrame();
final newRect = Rect.fromLTWH(
frame.left, frame.top, frame.width + 1, frame.height + 1);
await WindowController.fromWindowId(windowId).setFrame(newRect);
await wc.setFrame(newRect);
});
}
});

View File

@@ -69,8 +69,8 @@ class RustDeskMultiWindowManager {
// This function must be called in the main window thread.
// Because the _remoteDesktopWindows is managed in that thread.
openMonitorSession(
int windowId, String peerId, int display, int displayCount) async {
openMonitorSession(int windowId, String peerId, int display, int displayCount,
Rect? screenRect) async {
if (_remoteDesktopWindows.length > 1) {
for (final windowId in _remoteDesktopWindows) {
if (await DesktopMultiWindow.invokeMethod(
@@ -95,6 +95,14 @@ class RustDeskMultiWindowManager {
'display': display,
'displays': displays,
};
if (screenRect != null) {
params['screen_rect'] = {
'l': screenRect.left,
't': screenRect.top,
'r': screenRect.right,
'b': screenRect.bottom,
};
}
await _newSession(
false,
WindowType.RemoteDesktop,
@@ -102,21 +110,34 @@ class RustDeskMultiWindowManager {
peerId,
_remoteDesktopWindows,
jsonEncode(params),
screenRect: screenRect,
);
}
Future<int> newSessionWindow(
WindowType type, String remoteId, String msg, List<int> windows) async {
WindowType type,
String remoteId,
String msg,
List<int> windows,
bool withScreenRect,
) async {
final windowController = await DesktopMultiWindow.createWindow(msg);
final windowId = windowController.windowId;
windowController
..setFrame(
const Offset(0, 0) & Size(1280 + windowId * 20, 720 + windowId * 20))
..center()
..setTitle(getWindowNameWithId(
if (!withScreenRect) {
windowController
..setFrame(const Offset(0, 0) &
Size(1280 + windowId * 20, 720 + windowId * 20))
..center()
..setTitle(getWindowNameWithId(
remoteId,
overrideType: type,
));
} else {
windowController.setTitle(getWindowNameWithId(
remoteId,
overrideType: type,
));
}
if (Platform.isMacOS) {
Future.microtask(() => windowController.show());
}
@@ -131,11 +152,13 @@ class RustDeskMultiWindowManager {
String methodName,
String remoteId,
List<int> windows,
String msg,
) async {
String msg, {
Rect? screenRect,
}) async {
if (openInTabs) {
if (windows.isEmpty) {
final windowId = await newSessionWindow(type, remoteId, msg, windows);
final windowId = await newSessionWindow(
type, remoteId, msg, windows, screenRect != null);
return MultiWindowCallResult(windowId, null);
} else {
return call(type, methodName, msg);
@@ -144,8 +167,10 @@ class RustDeskMultiWindowManager {
if (_inactiveWindows.isNotEmpty) {
for (final windowId in windows) {
if (_inactiveWindows.contains(windowId)) {
await restoreWindowPosition(type,
windowId: windowId, peerId: remoteId);
if (screenRect == null) {
await restoreWindowPosition(type,
windowId: windowId, peerId: remoteId);
}
await DesktopMultiWindow.invokeMethod(windowId, methodName, msg);
WindowController.fromWindowId(windowId).show();
registerActiveWindow(windowId);
@@ -153,7 +178,8 @@ class RustDeskMultiWindowManager {
}
}
}
final windowId = await newSessionWindow(type, remoteId, msg, windows);
final windowId = await newSessionWindow(
type, remoteId, msg, windows, screenRect != null);
return MultiWindowCallResult(windowId, null);
}
}