* refact: multi_window_soft_rendering Signed-off-by: fufesou <linlong1266@gmail.com> * fix: window pos, potential wait for image Signed-off-by: fufesou <linlong1266@gmail.com> * comments Signed-off-by: fufesou <linlong1266@gmail.com> * remove debug print Signed-off-by: fufesou <linlong1266@gmail.com> * explicitly set rgba_data.size_got to false after init Signed-off-by: fufesou <linlong1266@gmail.com> * refact: multi window, merge images, render with texture Signed-off-by: fufesou <linlong1266@gmail.com> * revert, flutter.rs, rgba valid Signed-off-by: fufesou <linlong1266@gmail.com> * Add displays index before sending capture msg Signed-off-by: fufesou <linlong1266@gmail.com> * refact: multi window, soft rendering Signed-off-by: fufesou <linlong1266@gmail.com> * fix: build Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
		
			
				
	
	
		
			779 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			779 lines
		
	
	
		
			26 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/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});
 | |
| }
 | |
| 
 | |
| 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));
 | |
|   }
 | |
| 
 | |
|   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;
 | |
| }
 |