21pages 09466680d3
mobile virtual display, resolution menu, proxy setting ()
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>
2024-07-15 18:53:14 +08:00

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 [];
}