mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-05 00:36:14 +00:00
Add Without password actions and pass the selection through desktop, mobile and Web sessions. Omit the password when constructing LoginRequest, preserve saved credentials, and reuse the existing approval and OS login dialogs. Keep protocol and controlled-side authentication changes separate.
525 lines
18 KiB
Dart
525 lines
18 KiB
Dart
import 'dart:convert';
|
|
import 'dart:async';
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hbb/common.dart';
|
|
import 'package:flutter_hbb/common/shared_state.dart';
|
|
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
|
import 'package:flutter_hbb/consts.dart';
|
|
import 'package:flutter_hbb/models/input_model.dart';
|
|
import 'package:flutter_hbb/models/state_model.dart';
|
|
import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
|
|
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
|
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
|
|
as mod_menu;
|
|
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:bot_toast/bot_toast.dart';
|
|
|
|
import '../../models/platform_model.dart';
|
|
|
|
class _MenuTheme {
|
|
static const Color blueColor = MyTheme.button;
|
|
// kMinInteractiveDimension
|
|
static const double height = 20.0;
|
|
static const double dividerHeight = 12.0;
|
|
}
|
|
|
|
class ViewCameraTabPage extends StatefulWidget {
|
|
final Map<String, dynamic> params;
|
|
|
|
const ViewCameraTabPage({Key? key, required this.params}) : super(key: key);
|
|
|
|
@override
|
|
State<ViewCameraTabPage> createState() => _ViewCameraTabPageState(params);
|
|
}
|
|
|
|
class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
|
final tabController =
|
|
Get.put(DesktopTabController(tabType: DesktopTabType.viewCamera));
|
|
final contentKey = UniqueKey();
|
|
static const IconData selectedIcon = Icons.desktop_windows_sharp;
|
|
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
|
|
|
|
String? peerId;
|
|
bool _isScreenRectSet = false;
|
|
int? _display;
|
|
|
|
var connectionMap = RxList<Widget>.empty(growable: true);
|
|
|
|
_ViewCameraTabPageState(Map<String, dynamic> params) {
|
|
RemoteCountState.init();
|
|
peerId = params['id'];
|
|
final sessionId = params['session_id'];
|
|
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) {
|
|
final viewCameraPage = tabController.widget(id);
|
|
if (viewCameraPage is ViewCameraPage) {
|
|
final ffi = viewCameraPage.ffi;
|
|
bind.setCurSessionId(sessionId: ffi.sessionId);
|
|
}
|
|
WindowController.fromWindowId(params['windowId'])
|
|
.setTitle(getWindowNameWithId(id));
|
|
UnreadChatCountState.find(id).value = 0;
|
|
};
|
|
tabController.add(TabInfo(
|
|
key: peerId!,
|
|
label: peerId!,
|
|
selectedIcon: selectedIcon,
|
|
unselectedIcon: unselectedIcon,
|
|
onTabCloseButton: () async {
|
|
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
|
id: peerId!,
|
|
tabController: tabController,
|
|
)) {
|
|
return;
|
|
}
|
|
tabController.closeBy(peerId!);
|
|
},
|
|
page: ViewCameraPage(
|
|
key: ValueKey(peerId),
|
|
id: peerId!,
|
|
sessionId: sessionId == null ? null : SessionID(sessionId),
|
|
tabWindowId: tabWindowId,
|
|
display: display,
|
|
displays: displays?.cast<int>(),
|
|
password: params['password'],
|
|
loginMethod: params['loginMethod'] ?? '',
|
|
toolbarState: ToolbarState(),
|
|
tabController: tabController,
|
|
connToken: params['connToken'],
|
|
forceRelay: params['forceRelay'],
|
|
isSharedPassword: params['isSharedPassword'],
|
|
),
|
|
));
|
|
_update_remote_count();
|
|
}
|
|
tabController.onRemoved = (_, id) => onRemoveId(id);
|
|
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
if (!_isScreenRectSet) {
|
|
Future.delayed(Duration.zero, () {
|
|
restoreWindowPosition(
|
|
WindowType.ViewCamera,
|
|
windowId: windowId(),
|
|
peerId: tabController.state.value.tabs.isEmpty
|
|
? null
|
|
: tabController.state.value.tabs[0].key,
|
|
display: _display,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final child = Scaffold(
|
|
backgroundColor: Theme.of(context).colorScheme.background,
|
|
body: DesktopTab(
|
|
controller: tabController,
|
|
onWindowCloseButton: handleWindowCloseButton,
|
|
tail: const AddButton(),
|
|
selectedBorderColor: MyTheme.accent,
|
|
pageViewBuilder: (pageView) => pageView,
|
|
labelGetter: DesktopTab.tablabelGetter,
|
|
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
|
final connectionType = ConnectionTypeState.find(key);
|
|
if (!connectionType.isValid()) {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
icon,
|
|
label,
|
|
],
|
|
);
|
|
} else {
|
|
bool secure =
|
|
connectionType.secure.value == ConnectionType.strSecure;
|
|
bool direct =
|
|
connectionType.direct.value == ConnectionType.strDirect;
|
|
String msgConn = getConnectionText(
|
|
secure, direct, connectionType.stream_type.value);
|
|
var msgFingerprint = '${translate('Fingerprint')}:\n';
|
|
var fingerprint = FingerprintState.find(key).value;
|
|
if (fingerprint.isEmpty) {
|
|
fingerprint = 'N/A';
|
|
}
|
|
if (fingerprint.length > 5 * 8) {
|
|
var first = fingerprint.substring(0, 39);
|
|
var second = fingerprint.substring(40);
|
|
msgFingerprint += '$first\n$second';
|
|
} else {
|
|
msgFingerprint += fingerprint;
|
|
}
|
|
|
|
final tab = Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
icon,
|
|
Tooltip(
|
|
message: '$msgConn\n$msgFingerprint',
|
|
child: SvgPicture.asset(
|
|
'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
|
|
width: themeConf.iconSize,
|
|
height: themeConf.iconSize,
|
|
).paddingOnly(right: 5),
|
|
),
|
|
label,
|
|
unreadMessageCountBuilder(UnreadChatCountState.find(key))
|
|
.marginOnly(left: 4),
|
|
],
|
|
);
|
|
|
|
return Listener(
|
|
onPointerDown: (e) {
|
|
if (e.kind != ui.PointerDeviceKind.mouse) {
|
|
return;
|
|
}
|
|
final viewCameraPage = tabController.state.value.tabs
|
|
.firstWhere((tab) => tab.key == key)
|
|
.page as ViewCameraPage;
|
|
if (viewCameraPage.ffi.ffiModel.pi.isSet.isTrue &&
|
|
e.buttons == 2) {
|
|
showRightMenu(
|
|
(CancelFunc cancelFunc) {
|
|
return _tabMenuBuilder(key, cancelFunc);
|
|
},
|
|
target: e.position,
|
|
);
|
|
}
|
|
},
|
|
child: tab,
|
|
);
|
|
}
|
|
}),
|
|
),
|
|
);
|
|
final tabWidget = isLinux
|
|
? buildVirtualWindowFrame(context, child)
|
|
: workaroundWindowBorder(
|
|
context,
|
|
Obx(() => Container(
|
|
decoration: BoxDecoration(
|
|
border: Border.all(
|
|
color: MyTheme.color(context).border!,
|
|
width: stateGlobal.windowBorderWidth.value),
|
|
),
|
|
child: child,
|
|
)));
|
|
return isMacOS || kUseCompatibleUiMode
|
|
? tabWidget
|
|
: Obx(() => SubWindowDragToResizeArea(
|
|
key: contentKey,
|
|
child: tabWidget,
|
|
// Specially configured for a better resize area and remote control.
|
|
childPadding: kDragToResizeAreaPadding,
|
|
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
|
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
|
windowId: stateGlobal.windowId,
|
|
));
|
|
}
|
|
|
|
// Note: Some dup code to ../widgets/remote_toolbar
|
|
Widget _tabMenuBuilder(String key, CancelFunc cancelFunc) {
|
|
final List<MenuEntryBase<String>> menu = [];
|
|
const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0);
|
|
final viewCameraPage = tabController.state.value.tabs
|
|
.firstWhere((tab) => tab.key == key)
|
|
.page as ViewCameraPage;
|
|
final ffi = viewCameraPage.ffi;
|
|
final sessionId = ffi.sessionId;
|
|
final toolbarState = viewCameraPage.toolbarState;
|
|
menu.addAll([
|
|
MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Obx(() => Text(
|
|
translate(
|
|
toolbarState.hide.isTrue ? 'Show Toolbar' : 'Hide Toolbar'),
|
|
style: style,
|
|
)),
|
|
proc: () {
|
|
toolbarState.switchHide(sessionId);
|
|
cancelFunc();
|
|
},
|
|
padding: padding,
|
|
),
|
|
]);
|
|
|
|
if (tabController.state.value.tabs.length > 1) {
|
|
final splitAction = MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Text(
|
|
translate('Move tab to new window'),
|
|
style: style,
|
|
),
|
|
proc: () async {
|
|
await DesktopMultiWindow.invokeMethod(
|
|
kMainWindowId,
|
|
kWindowEventMoveTabToNewWindow,
|
|
'${windowId()},$key,$sessionId,ViewCamera');
|
|
cancelFunc();
|
|
},
|
|
padding: padding,
|
|
);
|
|
menu.insert(1, splitAction);
|
|
}
|
|
|
|
menu.addAll([
|
|
MenuEntryDivider<String>(),
|
|
MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Text(
|
|
translate('Copy Fingerprint'),
|
|
style: style,
|
|
),
|
|
proc: () => onCopyFingerprint(FingerprintState.find(key).value),
|
|
padding: padding,
|
|
dismissOnClicked: true,
|
|
dismissCallback: cancelFunc,
|
|
),
|
|
MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Text(
|
|
translate('Close'),
|
|
style: style,
|
|
),
|
|
proc: () async {
|
|
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
|
id: key,
|
|
tabController: tabController,
|
|
)) {
|
|
return;
|
|
}
|
|
tabController.closeBy(key);
|
|
cancelFunc();
|
|
},
|
|
padding: padding,
|
|
)
|
|
]);
|
|
|
|
return mod_menu.PopupMenu<String>(
|
|
items: menu
|
|
.map((entry) => entry.build(
|
|
context,
|
|
const MenuConfig(
|
|
commonColor: _MenuTheme.blueColor,
|
|
height: _MenuTheme.height,
|
|
dividerHeight: _MenuTheme.dividerHeight,
|
|
)))
|
|
.expand((i) => i)
|
|
.toList(),
|
|
);
|
|
}
|
|
|
|
void onRemoveId(String id) async {
|
|
if (tabController.state.value.tabs.isEmpty) {
|
|
// Keep calling until the window status is hidden.
|
|
//
|
|
// Workaround for Windows:
|
|
// If you click other buttons and close in msgbox within a very short period of time, the close may fail.
|
|
// `await WindowController.fromWindowId(windowId()).close();`.
|
|
Future<void> loopCloseWindow() async {
|
|
int c = 0;
|
|
final windowController = WindowController.fromWindowId(windowId());
|
|
while (c < 20 &&
|
|
tabController.state.value.tabs.isEmpty &&
|
|
(!await windowController.isHidden())) {
|
|
await windowController.close();
|
|
await Future.delayed(Duration(milliseconds: 100));
|
|
c++;
|
|
}
|
|
}
|
|
|
|
loopCloseWindow();
|
|
}
|
|
ConnectionTypeState.delete(id);
|
|
_update_remote_count();
|
|
}
|
|
|
|
int windowId() {
|
|
return widget.params["windowId"];
|
|
}
|
|
|
|
Future<bool> handleWindowCloseButton() async {
|
|
final connLength = tabController.length;
|
|
if (connLength == 1) {
|
|
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
|
id: tabController.state.value.tabs[0].key,
|
|
tabController: tabController,
|
|
)) {
|
|
return false;
|
|
}
|
|
}
|
|
if (connLength <= 1) {
|
|
tabController.clear();
|
|
return true;
|
|
} else {
|
|
final bool res;
|
|
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
|
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
|
res = true;
|
|
} else {
|
|
res = await closeConfirmDialog();
|
|
}
|
|
if (res) {
|
|
tabController.clear();
|
|
}
|
|
return res;
|
|
}
|
|
}
|
|
|
|
_update_remote_count() =>
|
|
RemoteCountState.find().value = tabController.length;
|
|
|
|
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
|
|
debugPrint(
|
|
"[View Camera Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
|
|
|
dynamic returnValue;
|
|
// for simplify, just replace connectionId
|
|
if (call.method == kWindowEventNewViewCamera) {
|
|
final args = jsonDecode(call.arguments);
|
|
final id = args['id'];
|
|
final sessionId = args['session_id'];
|
|
final tabWindowId = args['tab_window_id'];
|
|
final display = args['display'];
|
|
final displays = args['displays'];
|
|
final screenRect = parseParamScreenRect(args);
|
|
final prePeerCount = tabController.length;
|
|
Future.delayed(Duration.zero, () async {
|
|
if (stateGlobal.fullscreen.isTrue) {
|
|
await WindowController.fromWindowId(windowId()).setFullscreen(false);
|
|
stateGlobal.setFullscreen(false, procWnd: false);
|
|
}
|
|
await setNewConnectWindowFrame(windowId(), id!, prePeerCount,
|
|
WindowType.ViewCamera, display, screenRect);
|
|
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
|
|
await windowOnTop(windowId());
|
|
});
|
|
});
|
|
ConnectionTypeState.init(id);
|
|
tabController.add(TabInfo(
|
|
key: id,
|
|
label: id,
|
|
selectedIcon: selectedIcon,
|
|
unselectedIcon: unselectedIcon,
|
|
onTabCloseButton: () async {
|
|
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
|
id: id,
|
|
tabController: tabController,
|
|
)) {
|
|
return;
|
|
}
|
|
tabController.closeBy(id);
|
|
},
|
|
page: ViewCameraPage(
|
|
key: ValueKey(id),
|
|
id: id,
|
|
sessionId: sessionId == null ? null : SessionID(sessionId),
|
|
tabWindowId: tabWindowId,
|
|
display: display,
|
|
displays: displays?.cast<int>(),
|
|
password: args['password'],
|
|
loginMethod: args['loginMethod'] ?? '',
|
|
toolbarState: ToolbarState(),
|
|
tabController: tabController,
|
|
connToken: args['connToken'],
|
|
forceRelay: args['forceRelay'],
|
|
isSharedPassword: args['isSharedPassword'],
|
|
),
|
|
));
|
|
} else if (call.method == kWindowDisableGrabKeyboard) {
|
|
// ???
|
|
} else if (call.method == "onDestroy") {
|
|
tabController.clear();
|
|
} else if (call.method == kWindowActionRebuild) {
|
|
reloadCurrentWindow();
|
|
} else if (call.method == kWindowEventActiveSession) {
|
|
final jumpOk = tabController.jumpToByKey(call.arguments);
|
|
if (jumpOk) {
|
|
windowOnTop(windowId());
|
|
}
|
|
return jumpOk;
|
|
} else if (call.method == kWindowEventActiveDisplaySession) {
|
|
final args = jsonDecode(call.arguments);
|
|
final id = args['id'];
|
|
final display = args['display'];
|
|
final jumpOk =
|
|
tabController.jumpToByKeyAndDisplay(id, display, isCamera: true);
|
|
if (jumpOk) {
|
|
windowOnTop(windowId());
|
|
}
|
|
return jumpOk;
|
|
} else if (call.method == kWindowEventGetRemoteList) {
|
|
return tabController.state.value.tabs
|
|
.map((e) => e.key)
|
|
.toList()
|
|
.join(',');
|
|
} else if (call.method == kWindowEventGetSessionIdList) {
|
|
return tabController.state.value.tabs
|
|
.map((e) => '${e.key},${(e.page as ViewCameraPage).ffi.sessionId}')
|
|
.toList()
|
|
.join(';');
|
|
} else if (call.method == kWindowEventGetCachedSessionData) {
|
|
// Ready to show new window and close old tab.
|
|
final args = jsonDecode(call.arguments);
|
|
final id = args['id'];
|
|
final close = args['close'];
|
|
try {
|
|
final viewCameraPage = tabController.state.value.tabs
|
|
.firstWhere((tab) => tab.key == id)
|
|
.page as ViewCameraPage;
|
|
returnValue = viewCameraPage.ffi.ffiModel.cachedPeerData.toString();
|
|
} catch (e) {
|
|
debugPrint('Failed to get cached session data: $e');
|
|
}
|
|
if (close && returnValue != null) {
|
|
closeSessionOnDispose[id] = false;
|
|
tabController.closeBy(id);
|
|
}
|
|
} else if (call.method == kWindowEventRemoteWindowCoords) {
|
|
final viewCameraPage =
|
|
tabController.state.value.selectedTabInfo.page as ViewCameraPage;
|
|
final ffi = viewCameraPage.ffi;
|
|
final displayRect = ffi.ffiModel.displaysRect();
|
|
if (displayRect != null) {
|
|
final wc = WindowController.fromWindowId(windowId());
|
|
Rect? frame;
|
|
try {
|
|
frame = await wc.getFrame();
|
|
} catch (e) {
|
|
debugPrint(
|
|
"Failed to get frame of window $windowId, it may be hidden");
|
|
}
|
|
if (frame != null) {
|
|
ffi.cursorModel.moveLocal(0, 0);
|
|
final coords = RemoteWindowCoords(
|
|
frame,
|
|
CanvasCoords.fromCanvasModel(ffi.canvasModel),
|
|
CursorCoords.fromCursorModel(ffi.cursorModel),
|
|
displayRect);
|
|
returnValue = jsonEncode(coords.toJson());
|
|
}
|
|
}
|
|
} else if (call.method == kWindowEventSetFullscreen) {
|
|
stateGlobal.setFullscreen(call.arguments == 'true');
|
|
}
|
|
_update_remote_count();
|
|
return returnValue;
|
|
}
|
|
}
|