mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-04-09 00:51:29 +03:00
Merge remote-tracking branch 'upstream/master'
# Conflicts: # src/server/connection.rs
This commit is contained in:
@@ -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,133 @@ 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;
|
||||
},
|
||||
));
|
||||
},
|
||||
onSelected: (option) {
|
||||
setState(() {
|
||||
_idController.id = option.id;
|
||||
FocusScope.of(context).unfocus();
|
||||
});
|
||||
},
|
||||
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(onSelect: () => onSelected(peer), peer: peer)).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
);
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -1014,6 +978,10 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
ffi: widget.ffi,
|
||||
screenAdjustor: _screenAdjustor,
|
||||
),
|
||||
_VirtualDisplayMenu(
|
||||
id: widget.id,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
Divider(),
|
||||
toggles(),
|
||||
widget.pluginItem,
|
||||
@@ -1423,6 +1391,70 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
}
|
||||
}
|
||||
|
||||
class _VirtualDisplayMenu extends StatefulWidget {
|
||||
final String id;
|
||||
final FFI ffi;
|
||||
|
||||
_VirtualDisplayMenu({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.ffi,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_VirtualDisplayMenu> createState() => _VirtualDisplayMenuState();
|
||||
}
|
||||
|
||||
class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
|
||||
return Offstage();
|
||||
}
|
||||
if (!widget.ffi.ffiModel.pi.isInstalled) {
|
||||
return Offstage();
|
||||
}
|
||||
|
||||
final virtualDisplays = widget.ffi.ffiModel.pi.virtualDisplays;
|
||||
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
|
||||
children.add(CkbMenuButton(
|
||||
value: virtualDisplays.contains(i + 1),
|
||||
onChanged: (bool? value) async {
|
||||
if (value != null) {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: widget.ffi.sessionId, index: i + 1, on: value);
|
||||
}
|
||||
},
|
||||
child: Text('${translate('Virtual display')} ${i + 1}'),
|
||||
ffi: widget.ffi,
|
||||
));
|
||||
}
|
||||
children.add(Divider());
|
||||
children.add(MenuButton(
|
||||
onPressed: () {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: widget.ffi.sessionId,
|
||||
index: kAllVirtualDisplay,
|
||||
on: false);
|
||||
},
|
||||
ffi: widget.ffi,
|
||||
child: Text(translate('Plug out all')),
|
||||
));
|
||||
return _SubmenuButton(
|
||||
ffi: widget.ffi,
|
||||
menuChildren: children,
|
||||
child: Text(translate("Virtual display")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _KeyboardMenu extends StatelessWidget {
|
||||
final String id;
|
||||
final FFI ffi;
|
||||
@@ -1438,18 +1470,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 +1501,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 +1525,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 +1707,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 +1730,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 +1744,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 +2112,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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user