mirror of
https://github.com/rustdesk/rustdesk.git
synced 2024-12-25 21:27:55 +08:00
09466680d3
1. Merge code of mobile and desktop virtual display menu. 2. Mobile add seperate resolution menu, only support changing resolutions. 3. Android add proxy setting Signed-off-by: 21pages <sunboeasy@gmail.com>
909 lines
30 KiB
Dart
909 lines
30 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.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/desktop/widgets/remote_toolbar.dart';
|
|
import 'package:flutter_hbb/models/model.dart';
|
|
import 'package:flutter_hbb/models/platform_model.dart';
|
|
import 'package:get/get.dart';
|
|
|
|
bool isEditOsPassword = false;
|
|
|
|
class TTextMenu {
|
|
final Widget child;
|
|
final VoidCallback onPressed;
|
|
Widget? trailingIcon;
|
|
bool divider;
|
|
TTextMenu(
|
|
{required this.child,
|
|
required this.onPressed,
|
|
this.trailingIcon,
|
|
this.divider = false});
|
|
|
|
Widget getChild() {
|
|
if (trailingIcon != null) {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
child,
|
|
trailingIcon!,
|
|
],
|
|
);
|
|
} else {
|
|
return child;
|
|
}
|
|
}
|
|
}
|
|
|
|
class TRadioMenu<T> {
|
|
final Widget child;
|
|
final T value;
|
|
final T groupValue;
|
|
final ValueChanged<T?>? onChanged;
|
|
|
|
TRadioMenu(
|
|
{required this.child,
|
|
required this.value,
|
|
required this.groupValue,
|
|
required this.onChanged});
|
|
}
|
|
|
|
class TToggleMenu {
|
|
final Widget child;
|
|
final bool value;
|
|
final ValueChanged<bool?>? onChanged;
|
|
TToggleMenu(
|
|
{required this.child, required this.value, required this.onChanged});
|
|
}
|
|
|
|
handleOsPasswordEditIcon(
|
|
SessionID sessionId, OverlayDialogManager dialogManager) {
|
|
isEditOsPassword = true;
|
|
showSetOSPassword(
|
|
sessionId, false, dialogManager, null, () => isEditOsPassword = false);
|
|
}
|
|
|
|
handleOsPasswordAction(
|
|
SessionID sessionId, OverlayDialogManager dialogManager) async {
|
|
if (isEditOsPassword) {
|
|
isEditOsPassword = false;
|
|
return;
|
|
}
|
|
final password =
|
|
await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ??
|
|
'';
|
|
if (password.isEmpty) {
|
|
showSetOSPassword(sessionId, true, dialogManager, password,
|
|
() => isEditOsPassword = false);
|
|
} else {
|
|
bind.sessionInputOsPassword(sessionId: sessionId, value: password);
|
|
}
|
|
}
|
|
|
|
List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|
final ffiModel = ffi.ffiModel;
|
|
final pi = ffiModel.pi;
|
|
final perms = ffiModel.permissions;
|
|
final sessionId = ffi.sessionId;
|
|
|
|
List<TTextMenu> v = [];
|
|
// elevation
|
|
if (perms['keyboard'] != false && ffi.elevationModel.showRequestMenu) {
|
|
v.add(
|
|
TTextMenu(
|
|
child: Text(translate('Request Elevation')),
|
|
onPressed: () =>
|
|
showRequestElevationDialog(sessionId, ffi.dialogManager)),
|
|
);
|
|
}
|
|
// osAccount / osPassword
|
|
if (perms['keyboard'] != false) {
|
|
v.add(
|
|
TTextMenu(
|
|
child: Row(children: [
|
|
Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')),
|
|
]),
|
|
trailingIcon: Transform.scale(
|
|
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
|
|
child: IconButton(
|
|
onPressed: () {
|
|
if (isMobile && Navigator.canPop(context)) {
|
|
Navigator.pop(context);
|
|
}
|
|
if (pi.isHeadless) {
|
|
showSetOSAccount(sessionId, ffi.dialogManager);
|
|
} else {
|
|
handleOsPasswordEditIcon(sessionId, ffi.dialogManager);
|
|
}
|
|
},
|
|
icon: Icon(Icons.edit, color: isMobile ? MyTheme.accent : null),
|
|
),
|
|
),
|
|
onPressed: () => pi.isHeadless
|
|
? showSetOSAccount(sessionId, ffi.dialogManager)
|
|
: handleOsPasswordAction(sessionId, ffi.dialogManager),
|
|
),
|
|
);
|
|
}
|
|
// paste
|
|
if (isMobile &&
|
|
pi.platform != kPeerPlatformAndroid &&
|
|
perms['keyboard'] != false &&
|
|
perms['clipboard'] != false) {
|
|
v.add(TTextMenu(
|
|
child: Text(translate('Paste')),
|
|
onPressed: () async {
|
|
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
|
if (data != null && data.text != null) {
|
|
bind.sessionInputString(
|
|
sessionId: sessionId, value: data.text ?? "");
|
|
}
|
|
}));
|
|
}
|
|
// reset canvas
|
|
if (isMobile) {
|
|
v.add(TTextMenu(
|
|
child: Text(translate('Reset canvas')),
|
|
onPressed: () => ffi.cursorModel.reset()));
|
|
}
|
|
// transferFile
|
|
if (isDesktop) {
|
|
v.add(
|
|
TTextMenu(
|
|
child: Text(translate('Transfer file')),
|
|
onPressed: () => connect(context, id, isFileTransfer: true)),
|
|
);
|
|
}
|
|
// tcpTunneling
|
|
if (isDesktop) {
|
|
v.add(
|
|
TTextMenu(
|
|
child: Text(translate('TCP tunneling')),
|
|
onPressed: () => connect(context, id, isTcpTunneling: true)),
|
|
);
|
|
}
|
|
// note
|
|
if (bind
|
|
.sessionGetAuditServerSync(sessionId: sessionId, typ: "conn")
|
|
.isNotEmpty) {
|
|
v.add(
|
|
TTextMenu(
|
|
child: Text(translate('Note')),
|
|
onPressed: () => showAuditDialog(ffi)),
|
|
);
|
|
}
|
|
// divider
|
|
if (isDesktop || isWebDesktop) {
|
|
v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true));
|
|
}
|
|
// ctrlAltDel
|
|
if (!ffiModel.viewOnly &&
|
|
ffiModel.keyboard &&
|
|
(pi.platform == kPeerPlatformLinux || pi.sasEnabled)) {
|
|
v.add(
|
|
TTextMenu(
|
|
child: Text('${translate("Insert")} Ctrl + Alt + Del'),
|
|
onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)),
|
|
);
|
|
}
|
|
// restart
|
|
if (perms['restart'] != false &&
|
|
(pi.platform == kPeerPlatformLinux ||
|
|
pi.platform == kPeerPlatformWindows ||
|
|
pi.platform == kPeerPlatformMacOS)) {
|
|
v.add(
|
|
TTextMenu(
|
|
child: Text(translate('Restart remote device')),
|
|
onPressed: () =>
|
|
showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager)),
|
|
);
|
|
}
|
|
// insertLock
|
|
if (!ffiModel.viewOnly && ffi.ffiModel.keyboard) {
|
|
v.add(
|
|
TTextMenu(
|
|
child: Text(translate('Insert Lock')),
|
|
onPressed: () => bind.sessionLockScreen(sessionId: sessionId)),
|
|
);
|
|
}
|
|
// blockUserInput
|
|
if (ffi.ffiModel.keyboard &&
|
|
ffi.ffiModel.permissions['block_input'] != false &&
|
|
pi.platform == kPeerPlatformWindows) // privacy-mode != true ??
|
|
{
|
|
v.add(TTextMenu(
|
|
child: Obx(() => Text(translate(
|
|
'${BlockInputState.find(id).value ? 'Unb' : 'B'}lock user input'))),
|
|
onPressed: () {
|
|
RxBool blockInput = BlockInputState.find(id);
|
|
bind.sessionToggleOption(
|
|
sessionId: sessionId,
|
|
value: '${blockInput.value ? 'un' : ''}block-input');
|
|
blockInput.value = !blockInput.value;
|
|
}));
|
|
}
|
|
// switchSides
|
|
if (isDesktop &&
|
|
ffiModel.keyboard &&
|
|
pi.platform != kPeerPlatformAndroid &&
|
|
pi.platform != kPeerPlatformMacOS &&
|
|
versionCmp(pi.version, '1.2.0') >= 0 &&
|
|
bind.peerGetDefaultSessionsCount(id: id) == 1) {
|
|
v.add(TTextMenu(
|
|
child: Text(translate('Switch Sides')),
|
|
onPressed: () =>
|
|
showConfirmSwitchSidesDialog(sessionId, id, ffi.dialogManager)));
|
|
}
|
|
// refresh
|
|
if (pi.version.isNotEmpty) {
|
|
v.add(TTextMenu(
|
|
child: Text(translate('Refresh')),
|
|
onPressed: () => sessionRefreshVideo(sessionId, pi),
|
|
));
|
|
}
|
|
// record
|
|
if (!(isDesktop || isWeb) &&
|
|
(ffi.recordingModel.start || (perms["recording"] != false))) {
|
|
v.add(TTextMenu(
|
|
child: Row(
|
|
children: [
|
|
Text(translate(ffi.recordingModel.start
|
|
? 'Stop session recording'
|
|
: 'Start session recording')),
|
|
Padding(
|
|
padding: EdgeInsets.only(left: 12),
|
|
child: Icon(
|
|
ffi.recordingModel.start
|
|
? Icons.pause_circle_filled
|
|
: Icons.videocam_outlined,
|
|
color: MyTheme.accent),
|
|
)
|
|
],
|
|
),
|
|
onPressed: () => ffi.recordingModel.toggle()));
|
|
}
|
|
// fingerprint
|
|
if (!(isDesktop || isWebDesktop)) {
|
|
v.add(TTextMenu(
|
|
child: Text(translate('Copy Fingerprint')),
|
|
onPressed: () => onCopyFingerprint(FingerprintState.find(id).value),
|
|
));
|
|
}
|
|
return v;
|
|
}
|
|
|
|
Future<List<TRadioMenu<String>>> toolbarViewStyle(
|
|
BuildContext context, String id, FFI ffi) async {
|
|
final groupValue =
|
|
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
|
|
void onChanged(String? value) async {
|
|
if (value == null) return;
|
|
bind
|
|
.sessionSetViewStyle(sessionId: ffi.sessionId, value: value)
|
|
.then((_) => ffi.canvasModel.updateViewStyle());
|
|
}
|
|
|
|
return [
|
|
TRadioMenu<String>(
|
|
child: Text(translate('Scale original')),
|
|
value: kRemoteViewStyleOriginal,
|
|
groupValue: groupValue,
|
|
onChanged: onChanged),
|
|
TRadioMenu<String>(
|
|
child: Text(translate('Scale adaptive')),
|
|
value: kRemoteViewStyleAdaptive,
|
|
groupValue: groupValue,
|
|
onChanged: onChanged)
|
|
];
|
|
}
|
|
|
|
Future<List<TRadioMenu<String>>> toolbarImageQuality(
|
|
BuildContext context, String id, FFI ffi) async {
|
|
final groupValue =
|
|
await bind.sessionGetImageQuality(sessionId: ffi.sessionId) ?? '';
|
|
onChanged(String? value) async {
|
|
if (value == null) return;
|
|
await bind.sessionSetImageQuality(sessionId: ffi.sessionId, value: value);
|
|
}
|
|
|
|
return [
|
|
TRadioMenu<String>(
|
|
child: Text(translate('Good image quality')),
|
|
value: kRemoteImageQualityBest,
|
|
groupValue: groupValue,
|
|
onChanged: onChanged),
|
|
TRadioMenu<String>(
|
|
child: Text(translate('Balanced')),
|
|
value: kRemoteImageQualityBalanced,
|
|
groupValue: groupValue,
|
|
onChanged: onChanged),
|
|
TRadioMenu<String>(
|
|
child: Text(translate('Optimize reaction time')),
|
|
value: kRemoteImageQualityLow,
|
|
groupValue: groupValue,
|
|
onChanged: onChanged),
|
|
TRadioMenu<String>(
|
|
child: Text(translate('Custom')),
|
|
value: kRemoteImageQualityCustom,
|
|
groupValue: groupValue,
|
|
onChanged: (value) {
|
|
onChanged(value);
|
|
customImageQualityDialog(ffi.sessionId, id, ffi);
|
|
},
|
|
),
|
|
];
|
|
}
|
|
|
|
Future<List<TRadioMenu<String>>> toolbarCodec(
|
|
BuildContext context, String id, FFI ffi) async {
|
|
final sessionId = ffi.sessionId;
|
|
final alternativeCodecs =
|
|
await bind.sessionAlternativeCodecs(sessionId: sessionId);
|
|
final groupValue = await bind.sessionGetOption(
|
|
sessionId: sessionId, arg: kOptionCodecPreference) ??
|
|
'';
|
|
final List<bool> codecs = [];
|
|
try {
|
|
final Map codecsJson = jsonDecode(alternativeCodecs);
|
|
final vp8 = codecsJson['vp8'] ?? false;
|
|
final av1 = codecsJson['av1'] ?? false;
|
|
final h264 = codecsJson['h264'] ?? false;
|
|
final h265 = codecsJson['h265'] ?? false;
|
|
codecs.add(vp8);
|
|
codecs.add(av1);
|
|
codecs.add(h264);
|
|
codecs.add(h265);
|
|
} catch (e) {
|
|
debugPrint("Show Codec Preference err=$e");
|
|
}
|
|
final visible =
|
|
codecs.length == 4 && (codecs[0] || codecs[1] || codecs[2] || codecs[3]);
|
|
if (!visible) return [];
|
|
onChanged(String? value) async {
|
|
if (value == null) return;
|
|
await bind.sessionPeerOption(
|
|
sessionId: sessionId, name: kOptionCodecPreference, value: value);
|
|
bind.sessionChangePreferCodec(sessionId: sessionId);
|
|
}
|
|
|
|
TRadioMenu<String> radio(String label, String value, bool enabled) {
|
|
return TRadioMenu<String>(
|
|
child: Text(label),
|
|
value: value,
|
|
groupValue: groupValue,
|
|
onChanged: enabled ? onChanged : null);
|
|
}
|
|
|
|
var autoLabel = translate('Auto');
|
|
if (groupValue == 'auto' &&
|
|
ffi.qualityMonitorModel.data.codecFormat != null) {
|
|
autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})';
|
|
}
|
|
return [
|
|
radio(autoLabel, 'auto', true),
|
|
if (codecs[0]) radio('VP8', 'vp8', codecs[0]),
|
|
radio('VP9', 'vp9', true),
|
|
if (codecs[1]) radio('AV1', 'av1', codecs[1]),
|
|
if (codecs[2]) radio('H264', 'h264', codecs[2]),
|
|
if (codecs[3]) radio('H265', 'h265', codecs[3]),
|
|
];
|
|
}
|
|
|
|
Future<List<TToggleMenu>> toolbarCursor(
|
|
BuildContext context, String id, FFI ffi) async {
|
|
List<TToggleMenu> v = [];
|
|
final ffiModel = ffi.ffiModel;
|
|
final pi = ffiModel.pi;
|
|
final sessionId = ffi.sessionId;
|
|
|
|
// show remote cursor
|
|
if (pi.platform != kPeerPlatformAndroid &&
|
|
!ffi.canvasModel.cursorEmbedded &&
|
|
!pi.isWayland) {
|
|
final state = ShowRemoteCursorState.find(id);
|
|
final lockState = ShowRemoteCursorLockState.find(id);
|
|
final enabled = !ffiModel.viewOnly;
|
|
final option = 'show-remote-cursor';
|
|
if (pi.currentDisplay == kAllDisplayValue ||
|
|
bind.sessionIsMultiUiSession(sessionId: sessionId)) {
|
|
lockState.value = false;
|
|
}
|
|
v.add(TToggleMenu(
|
|
child: Text(translate('Show remote cursor')),
|
|
value: state.value,
|
|
onChanged: enabled && !lockState.value
|
|
? (value) async {
|
|
if (value == null) return;
|
|
await bind.sessionToggleOption(
|
|
sessionId: sessionId, value: option);
|
|
state.value = bind.sessionGetToggleOptionSync(
|
|
sessionId: sessionId, arg: option);
|
|
}
|
|
: null));
|
|
}
|
|
// follow remote cursor
|
|
if (pi.platform != kPeerPlatformAndroid &&
|
|
!ffi.canvasModel.cursorEmbedded &&
|
|
!pi.isWayland &&
|
|
versionCmp(pi.version, "1.2.4") >= 0 &&
|
|
pi.displays.length > 1 &&
|
|
pi.currentDisplay != kAllDisplayValue &&
|
|
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
|
|
final option = 'follow-remote-cursor';
|
|
final value =
|
|
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
|
final showCursorOption = 'show-remote-cursor';
|
|
final showCursorState = ShowRemoteCursorState.find(id);
|
|
final showCursorLockState = ShowRemoteCursorLockState.find(id);
|
|
final showCursorEnabled = bind.sessionGetToggleOptionSync(
|
|
sessionId: sessionId, arg: showCursorOption);
|
|
showCursorLockState.value = value;
|
|
if (value && !showCursorEnabled) {
|
|
await bind.sessionToggleOption(
|
|
sessionId: sessionId, value: showCursorOption);
|
|
showCursorState.value = bind.sessionGetToggleOptionSync(
|
|
sessionId: sessionId, arg: showCursorOption);
|
|
}
|
|
v.add(TToggleMenu(
|
|
child: Text(translate('Follow remote cursor')),
|
|
value: value,
|
|
onChanged: (value) async {
|
|
if (value == null) return;
|
|
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
|
value = bind.sessionGetToggleOptionSync(
|
|
sessionId: sessionId, arg: option);
|
|
showCursorLockState.value = value;
|
|
if (!showCursorEnabled) {
|
|
await bind.sessionToggleOption(
|
|
sessionId: sessionId, value: showCursorOption);
|
|
showCursorState.value = bind.sessionGetToggleOptionSync(
|
|
sessionId: sessionId, arg: showCursorOption);
|
|
}
|
|
}));
|
|
}
|
|
// follow remote window focus
|
|
if (pi.platform != kPeerPlatformAndroid &&
|
|
!ffi.canvasModel.cursorEmbedded &&
|
|
!pi.isWayland &&
|
|
versionCmp(pi.version, "1.2.4") >= 0 &&
|
|
pi.displays.length > 1 &&
|
|
pi.currentDisplay != kAllDisplayValue &&
|
|
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
|
|
final option = 'follow-remote-window';
|
|
final value =
|
|
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
|
v.add(TToggleMenu(
|
|
child: Text(translate('Follow remote window focus')),
|
|
value: value,
|
|
onChanged: (value) async {
|
|
if (value == null) return;
|
|
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
|
value = bind.sessionGetToggleOptionSync(
|
|
sessionId: sessionId, arg: option);
|
|
}));
|
|
}
|
|
// zoom cursor
|
|
final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? '';
|
|
if (!isMobile &&
|
|
pi.platform != kPeerPlatformAndroid &&
|
|
viewStyle != kRemoteViewStyleOriginal) {
|
|
final option = 'zoom-cursor';
|
|
final peerState = PeerBoolOption.find(id, option);
|
|
v.add(TToggleMenu(
|
|
child: Text(translate('Zoom cursor')),
|
|
value: peerState.value,
|
|
onChanged: (value) async {
|
|
if (value == null) return;
|
|
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
|
peerState.value =
|
|
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
|
},
|
|
));
|
|
}
|
|
return v;
|
|
}
|
|
|
|
Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|
BuildContext context, String id, FFI ffi) async {
|
|
List<TToggleMenu> v = [];
|
|
final ffiModel = ffi.ffiModel;
|
|
final pi = ffiModel.pi;
|
|
final perms = ffiModel.permissions;
|
|
final sessionId = ffi.sessionId;
|
|
|
|
// show quality monitor
|
|
final option = 'show-quality-monitor';
|
|
v.add(TToggleMenu(
|
|
value: bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option),
|
|
onChanged: (value) async {
|
|
if (value == null) return;
|
|
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
|
ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
|
},
|
|
child: Text(translate('Show quality monitor'))));
|
|
// mute
|
|
if (perms['audio'] != false) {
|
|
final option = 'disable-audio';
|
|
final value =
|
|
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
|
v.add(TToggleMenu(
|
|
value: value,
|
|
onChanged: (value) {
|
|
if (value == null) return;
|
|
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
|
},
|
|
child: Text(translate('Mute'))));
|
|
}
|
|
// file copy and paste
|
|
// If the version is less than 1.2.4, file copy and paste is supported on Windows only.
|
|
final isSupportIfPeer_1_2_3 = versionCmp(pi.version, '1.2.4') < 0 &&
|
|
isWindows &&
|
|
pi.platform == kPeerPlatformWindows;
|
|
// If the version is 1.2.4 or later, file copy and paste is supported when kPlatformAdditionsHasFileClipboard is set.
|
|
final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 &&
|
|
bind.mainHasFileClipboard() &&
|
|
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard);
|
|
if (ffiModel.keyboard &&
|
|
perms['file'] != false &&
|
|
(isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
|
|
final enabled = !ffiModel.viewOnly;
|
|
final value = bind.sessionGetToggleOptionSync(
|
|
sessionId: sessionId, arg: kOptionEnableFileCopyPaste);
|
|
v.add(TToggleMenu(
|
|
value: value,
|
|
onChanged: enabled
|
|
? (value) {
|
|
if (value == null) return;
|
|
bind.sessionToggleOption(
|
|
sessionId: sessionId, value: kOptionEnableFileCopyPaste);
|
|
}
|
|
: null,
|
|
child: Text(translate('Enable file copy and paste'))));
|
|
}
|
|
// disable clipboard
|
|
if (ffiModel.keyboard && perms['clipboard'] != false) {
|
|
final enabled = !ffiModel.viewOnly;
|
|
final option = 'disable-clipboard';
|
|
var value =
|
|
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
|
if (ffiModel.viewOnly) value = true;
|
|
v.add(TToggleMenu(
|
|
value: value,
|
|
onChanged: enabled
|
|
? (value) {
|
|
if (value == null) return;
|
|
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
|
}
|
|
: null,
|
|
child: Text(translate('Disable clipboard'))));
|
|
}
|
|
// lock after session end
|
|
if (ffiModel.keyboard && !ffiModel.isPeerAndroid) {
|
|
final enabled = !ffiModel.viewOnly;
|
|
final option = 'lock-after-session-end';
|
|
final value =
|
|
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
|
v.add(TToggleMenu(
|
|
value: value,
|
|
onChanged: enabled
|
|
? (value) {
|
|
if (value == null) return;
|
|
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
|
}
|
|
: null,
|
|
child: Text(translate('Lock after session end'))));
|
|
}
|
|
|
|
if (pi.isSupportMultiDisplay &&
|
|
PrivacyModeState.find(id).isEmpty &&
|
|
pi.displaysCount.value > 1 &&
|
|
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
|
|
final value =
|
|
bind.sessionGetDisplaysAsIndividualWindows(sessionId: ffi.sessionId) ==
|
|
'Y';
|
|
v.add(TToggleMenu(
|
|
value: value,
|
|
onChanged: (value) {
|
|
if (value == null) return;
|
|
bind.sessionSetDisplaysAsIndividualWindows(
|
|
sessionId: sessionId, value: value ? 'Y' : 'N');
|
|
},
|
|
child: Text(translate('Show displays as individual windows'))));
|
|
}
|
|
|
|
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
|
|
if (pi.isSupportMultiDisplay && isMultiScreens) {
|
|
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' : 'N');
|
|
},
|
|
child: Text(translate('Use all my displays for the remote session'))));
|
|
}
|
|
|
|
// 444
|
|
final codec_format = ffi.qualityMonitorModel.data.codecFormat;
|
|
if (versionCmp(pi.version, "1.2.4") >= 0 &&
|
|
(codec_format == "AV1" || codec_format == "VP9")) {
|
|
final option = 'i444';
|
|
final value =
|
|
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
|
v.add(TToggleMenu(
|
|
value: value,
|
|
onChanged: (value) async {
|
|
if (value == null) return;
|
|
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
|
bind.sessionChangePreferCodec(sessionId: sessionId);
|
|
},
|
|
child: Text(translate('True color (4:4:4)'))));
|
|
}
|
|
|
|
if (isMobile) {
|
|
v.addAll(toolbarKeyboardToggles(ffi));
|
|
}
|
|
|
|
// view mode (mobile only, desktop is in keyboard menu)
|
|
if (isMobile && versionCmp(pi.version, '1.2.0') >= 0) {
|
|
v.add(TToggleMenu(
|
|
value: ffiModel.viewOnly,
|
|
onChanged: (value) async {
|
|
if (value == null) return;
|
|
await bind.sessionToggleOption(
|
|
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
|
ffiModel.setViewOnly(id, value);
|
|
},
|
|
child: Text(translate('View Mode'))));
|
|
}
|
|
return v;
|
|
}
|
|
|
|
var togglePrivacyModeTime = DateTime.now().subtract(const Duration(hours: 1));
|
|
|
|
List<TToggleMenu> toolbarPrivacyMode(
|
|
RxString privacyModeState, BuildContext context, String id, FFI ffi) {
|
|
final ffiModel = ffi.ffiModel;
|
|
final pi = ffiModel.pi;
|
|
final sessionId = ffi.sessionId;
|
|
|
|
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc) {
|
|
final enabled = !ffi.ffiModel.viewOnly;
|
|
return TToggleMenu(
|
|
value: privacyModeState.isNotEmpty,
|
|
onChanged: enabled
|
|
? (value) {
|
|
if (value == null) return;
|
|
if (ffiModel.pi.currentDisplay != 0 &&
|
|
ffiModel.pi.currentDisplay != kAllDisplayValue) {
|
|
msgBox(
|
|
sessionId,
|
|
'custom-nook-nocancel-hasclose',
|
|
'info',
|
|
'Please switch to Display 1 first',
|
|
'',
|
|
ffi.dialogManager);
|
|
return;
|
|
}
|
|
final option = 'privacy-mode';
|
|
toggleFunc(sessionId, option);
|
|
}
|
|
: null,
|
|
child: Text(translate('Privacy mode')));
|
|
}
|
|
|
|
final privacyModeImpls =
|
|
pi.platformAdditions[kPlatformAdditionsSupportedPrivacyModeImpl]
|
|
as List<dynamic>?;
|
|
if (privacyModeImpls == null) {
|
|
return [
|
|
getDefaultMenu((sid, opt) async {
|
|
bind.sessionToggleOption(sessionId: sid, value: opt);
|
|
togglePrivacyModeTime = DateTime.now();
|
|
})
|
|
];
|
|
}
|
|
if (privacyModeImpls.isEmpty) {
|
|
return [];
|
|
}
|
|
|
|
if (privacyModeImpls.length == 1) {
|
|
final implKey = (privacyModeImpls[0] as List<dynamic>)[0] as String;
|
|
return [
|
|
getDefaultMenu((sid, opt) async {
|
|
bind.sessionTogglePrivacyMode(
|
|
sessionId: sid, implKey: implKey, on: privacyModeState.isEmpty);
|
|
togglePrivacyModeTime = DateTime.now();
|
|
})
|
|
];
|
|
} else {
|
|
return privacyModeImpls.map((e) {
|
|
final implKey = (e as List<dynamic>)[0] as String;
|
|
final implName = (e)[1] as String;
|
|
return TToggleMenu(
|
|
child: Text(translate(implName)),
|
|
value: privacyModeState.value == implKey,
|
|
onChanged: (value) {
|
|
if (value == null) return;
|
|
togglePrivacyModeTime = DateTime.now();
|
|
bind.sessionTogglePrivacyMode(
|
|
sessionId: sessionId, implKey: implKey, on: value);
|
|
});
|
|
}).toList();
|
|
}
|
|
}
|
|
|
|
List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
|
|
final ffiModel = ffi.ffiModel;
|
|
final pi = ffiModel.pi;
|
|
final sessionId = ffi.sessionId;
|
|
List<TToggleMenu> v = [];
|
|
|
|
// swap key
|
|
if (ffiModel.keyboard &&
|
|
((isMacOS && pi.platform != kPeerPlatformMacOS) ||
|
|
(!isMacOS && pi.platform == kPeerPlatformMacOS))) {
|
|
final option = 'allow_swap_key';
|
|
final value =
|
|
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
|
onChanged(bool? value) {
|
|
if (value == null) return;
|
|
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
|
}
|
|
|
|
final enabled = !ffi.ffiModel.viewOnly;
|
|
v.add(TToggleMenu(
|
|
value: value,
|
|
onChanged: enabled ? onChanged : null,
|
|
child: Text(translate('Swap control-command key'))));
|
|
}
|
|
|
|
// reverse mouse wheel
|
|
if (ffiModel.keyboard) {
|
|
var optionValue =
|
|
bind.sessionGetReverseMouseWheelSync(sessionId: sessionId) ?? '';
|
|
if (optionValue == '') {
|
|
optionValue = bind.mainGetUserDefaultOption(key: kKeyReverseMouseWheel);
|
|
}
|
|
onChanged(bool? value) async {
|
|
if (value == null) return;
|
|
await bind.sessionSetReverseMouseWheel(
|
|
sessionId: sessionId, value: value ? 'Y' : 'N');
|
|
}
|
|
|
|
final enabled = !ffi.ffiModel.viewOnly;
|
|
v.add(TToggleMenu(
|
|
value: optionValue == 'Y',
|
|
onChanged: enabled ? onChanged : null,
|
|
child: Text(translate('Reverse mouse wheel'))));
|
|
}
|
|
|
|
// swap left right mouse
|
|
if (ffiModel.keyboard) {
|
|
final option = 'swap-left-right-mouse';
|
|
final value =
|
|
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
|
onChanged(bool? value) {
|
|
if (value == null) return;
|
|
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
|
}
|
|
|
|
final enabled = !ffi.ffiModel.viewOnly;
|
|
v.add(TToggleMenu(
|
|
value: value,
|
|
onChanged: enabled ? onChanged : null,
|
|
child: Text(translate('swap-left-right-mouse'))));
|
|
}
|
|
return v;
|
|
}
|
|
|
|
bool showVirtualDisplayMenu(FFI ffi) {
|
|
if (ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
|
|
return false;
|
|
}
|
|
if (!ffi.ffiModel.pi.isInstalled) {
|
|
return false;
|
|
}
|
|
if (ffi.ffiModel.pi.isRustDeskIdd || ffi.ffiModel.pi.isAmyuniIdd) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
List<Widget> getVirtualDisplayMenuChildren(
|
|
FFI ffi, String id, VoidCallback? clickCallBack) {
|
|
if (!showVirtualDisplayMenu(ffi)) {
|
|
return [];
|
|
}
|
|
final pi = ffi.ffiModel.pi;
|
|
final privacyModeState = PrivacyModeState.find(id);
|
|
if (pi.isRustDeskIdd) {
|
|
final virtualDisplays = ffi.ffiModel.pi.RustDeskVirtualDisplays;
|
|
final children = <Widget>[];
|
|
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
|
|
children.add(Obx(() => CkbMenuButton(
|
|
value: virtualDisplays.contains(i + 1),
|
|
onChanged: privacyModeState.isNotEmpty
|
|
? null
|
|
: (bool? value) async {
|
|
if (value != null) {
|
|
bind.sessionToggleVirtualDisplay(
|
|
sessionId: ffi.sessionId, index: i + 1, on: value);
|
|
clickCallBack?.call();
|
|
}
|
|
},
|
|
child: Text('${translate('Virtual display')} ${i + 1}'),
|
|
ffi: ffi,
|
|
)));
|
|
}
|
|
children.add(Divider());
|
|
children.add(Obx(() => MenuButton(
|
|
onPressed: privacyModeState.isNotEmpty
|
|
? null
|
|
: () {
|
|
bind.sessionToggleVirtualDisplay(
|
|
sessionId: ffi.sessionId,
|
|
index: kAllVirtualDisplay,
|
|
on: false);
|
|
clickCallBack?.call();
|
|
},
|
|
ffi: ffi,
|
|
child: Text(translate('Plug out all')),
|
|
)));
|
|
return children;
|
|
}
|
|
if (pi.isAmyuniIdd) {
|
|
final count = ffi.ffiModel.pi.amyuniVirtualDisplayCount;
|
|
final children = <Widget>[
|
|
Obx(() => Row(
|
|
children: [
|
|
TextButton(
|
|
onPressed: privacyModeState.isNotEmpty || count == 0
|
|
? null
|
|
: () {
|
|
bind.sessionToggleVirtualDisplay(
|
|
sessionId: ffi.sessionId, index: 0, on: false);
|
|
clickCallBack?.call();
|
|
},
|
|
child: Icon(Icons.remove),
|
|
),
|
|
Text(count.toString()),
|
|
TextButton(
|
|
onPressed: privacyModeState.isNotEmpty || count == 4
|
|
? null
|
|
: () {
|
|
bind.sessionToggleVirtualDisplay(
|
|
sessionId: ffi.sessionId, index: 0, on: true);
|
|
clickCallBack?.call();
|
|
},
|
|
child: Icon(Icons.add),
|
|
),
|
|
],
|
|
)),
|
|
Divider(),
|
|
Obx(() => MenuButton(
|
|
onPressed: privacyModeState.isNotEmpty || count == 0
|
|
? null
|
|
: () {
|
|
bind.sessionToggleVirtualDisplay(
|
|
sessionId: ffi.sessionId,
|
|
index: kAllVirtualDisplay,
|
|
on: false);
|
|
clickCallBack?.call();
|
|
},
|
|
ffi: ffi,
|
|
child: Text(translate('Plug out all')),
|
|
)),
|
|
];
|
|
return children;
|
|
}
|
|
return [];
|
|
}
|