import 'dart:convert'; import 'dart:io'; 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/models/model.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/desktop_render_texture.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}); } class TRadioMenu { final Widget child; final T value; final T groupValue; final ValueChanged? onChanged; TRadioMenu( {required this.child, required this.value, required this.groupValue, required this.onChanged}); } class TToggleMenu { final Widget child; final bool value; final ValueChanged? 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 toolbarControls(BuildContext context, String id, FFI ffi) { final ffiModel = ffi.ffiModel; final pi = ffiModel.pi; final perms = ffiModel.permissions; final sessionId = ffi.sessionId; List v = []; // elevation if (perms['keyboard'] != false && ffi.elevationModel.showRequestMenu) { v.add( TTextMenu( child: Text(translate('Request Elevation')), onPressed: () => showRequestElevationDialog(sessionId, ffi.dialogManager)), ); } // osAccount / osPassword v.add( TTextMenu( child: Row(children: [ Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')), Offstage( offstage: isDesktop, child: Icon(Icons.edit, color: MyTheme.accent).marginOnly(left: 12), ) ]), trailingIcon: Transform.scale( scale: 0.8, child: InkWell( onTap: () => pi.isHeadless ? showSetOSAccount(sessionId, ffi.dialogManager) : handleOsPasswordEditIcon(sessionId, ffi.dialogManager), child: Icon(Icons.edit), ), ), onPressed: () => pi.isHeadless ? showSetOSAccount(sessionId, ffi.dialogManager) : handleOsPasswordAction(sessionId, ffi.dialogManager), ), ); // paste if (isMobile && 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) { 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 && 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 var codecFormat = ffi.qualityMonitorModel.data.codecFormat; if (!isDesktop && (ffi.recordingModel.start || (perms["recording"] != false && (codecFormat == "VP8" || codecFormat == "VP9")))) { 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) { v.add(TTextMenu( child: Text(translate('Copy Fingerprint')), onPressed: () => onCopyFingerprint(FingerprintState.find(id).value), )); } return v; } Future>> 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( child: Text(translate('Scale original')), value: kRemoteViewStyleOriginal, groupValue: groupValue, onChanged: onChanged), TRadioMenu( child: Text(translate('Scale adaptive')), value: kRemoteViewStyleAdaptive, groupValue: groupValue, onChanged: onChanged) ]; } Future>> 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( child: Text(translate('Good image quality')), value: kRemoteImageQualityBest, groupValue: groupValue, onChanged: onChanged), TRadioMenu( child: Text(translate('Balanced')), value: kRemoteImageQualityBalanced, groupValue: groupValue, onChanged: onChanged), TRadioMenu( child: Text(translate('Optimize reaction time')), value: kRemoteImageQualityLow, groupValue: groupValue, onChanged: onChanged), TRadioMenu( child: Text(translate('Custom')), value: kRemoteImageQualityCustom, groupValue: groupValue, onChanged: (value) { onChanged(value); customImageQualityDialog(ffi.sessionId, id, ffi); }, ), ]; } Future>> 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: 'codec-preference') ?? ''; final List 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: 'codec-preference', value: value); bind.sessionChangePreferCodec(sessionId: sessionId); } TRadioMenu radio(String label, String value, bool enabled) { return TRadioMenu( child: Text(translate(label)), value: value, groupValue: groupValue, onChanged: enabled ? onChanged : null); } return [ radio('Auto', '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> toolbarDisplayToggle( BuildContext context, String id, FFI ffi) async { List v = []; final ffiModel = ffi.ffiModel; final pi = ffiModel.pi; final perms = ffiModel.permissions; final sessionId = ffi.sessionId; // show remote cursor if (pi.platform != kPeerPlatformAndroid && !ffi.canvasModel.cursorEmbedded && !pi.isWayland) { final state = ShowRemoteCursorState.find(id); final enabled = !ffiModel.viewOnly; final option = 'show-remote-cursor'; v.add(TToggleMenu( child: Text(translate('Show remote cursor')), value: state.value, onChanged: enabled ? (value) async { if (value == null) return; await bind.sessionToggleOption( sessionId: sessionId, value: option); state.value = bind.sessionGetToggleOptionSync( sessionId: sessionId, arg: option); } : null)); } // 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); }, )); } // 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 (Platform.isWindows && pi.platform == kPeerPlatformWindows && perms['file'] != false) { final option = 'enable-file-transfer'; 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('Allow 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) { final option = 'lock-after-session-end'; 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('Lock after session end')))); } // privacy mode if (ffiModel.keyboard && pi.features.privacyMode) { final option = 'privacy-mode'; final rxValue = PrivacyModeState.find(id); v.add(TToggleMenu( value: rxValue.value, onChanged: (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; } bind.sessionToggleOption(sessionId: sessionId, value: option); }, child: Text(translate('Privacy mode')))); } // swap key if (ffiModel.keyboard && ((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) || (!Platform.isMacOS && pi.platform == kPeerPlatformMacOS))) { final option = 'allow_swap_key'; 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('Swap control-command key')))); } if (useTextureRender && pi.isSupportMultiDisplay && PrivacyModeState.find(id).isFalse && 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' : ''); }, child: Text(translate('Show displays as individual windows')))); } return v; }