mirror of
https://github.com/rustdesk/rustdesk.git
synced 2025-12-13 03:28:03 +00:00
* feat(ui): custom scale mode with inline controls and live apply Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * Update flutter/lib/models/model.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update flutter/lib/desktop/widgets/remote_toolbar.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * refactor(dialog): remove unused showCustomScaleDialog function Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * feat(ui): enhance custom scale controls with live updates and improved UI - Introduced a reactive custom scale percentage using RxInt. - Added initialization of custom scale from stored options after the widget builds. - Updated viewStyle method to conditionally display custom controls based on selection. - Implemented a debouncer for smoother scale adjustments. - Enhanced slider UI with custom thumb shape and improved button interactions. This update improves user experience by allowing real-time adjustments to the custom scale settings. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * refactor(remote_toolbar): improve widget lifecycle management and enhance slider dimensions - Moved initialization of custom scale percentage to initState for better lifecycle handling. - Updated slider thumb dimensions and layout for improved UI consistency. - Added dispose method to clean up resources in custom scale controls. These changes enhance the overall performance and user experience of the remote toolbar. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * feat(remote_toolbar): enhance scroll behavior and improve slider thumb rendering - Introduced a new state variable to manage scroll enablement based on canvas model changes. - Updated the return value of the viewStyle method to include the scroll enablement status. - Refactored the slider thumb shape for better performance and visual consistency. - Improved the initialization of image overflow detection in the CanvasModel. These changes enhance the user experience by providing dynamic scroll control and a more responsive UI. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * Update flutter/lib/desktop/widgets/remote_toolbar.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * refactor(scale): introduce utility functions for custom scale management for DRY - Added a new file `scale.dart` containing utility functions to clamp, parse, and compute custom scale percentages. - Refactored the `CanvasModel` and `_DisplayMenuState` to utilize the new utility functions for fetching and applying custom scale settings. - Improved code readability and maintainability by centralizing scale-related logic. These changes enhance the handling of custom scale settings across the application. Signed-off-by: Alessandro De Blasis alex@deblasis.net Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * Update flutter/lib/utils/scale.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update flutter/lib/models/model.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update flutter/lib/desktop/widgets/remote_toolbar.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update flutter/lib/desktop/widgets/remote_toolbar.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update flutter/lib/desktop/widgets/remote_toolbar.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update flutter/lib/models/model.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: Remove unused import of 'uuid' in scale.dart Signed-off-by: Alessandro De Blasis alex@deblasis.net Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * feat(remote_toolbar): implement nonlinear mapping for custom scale slider - Added piecewise mapping functions to convert normalized slider positions to custom scale percentages and vice versa. - Introduced snapping behavior for the slider to enhance user experience. - Updated the slider's minimum and maximum values to align with the new mapping logic. - Adjusted the clamping function to ensure the minimum percentage is 10. These changes improve the precision and usability of the custom scale slider in the remote toolbar. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * fix(scale): update minimum scale percentage to 5 - Adjusted the minimum scale percentage in both the remote toolbar and the clamping function to improve consistency and usability. - This change aligns the clamping logic with the updated minimum value for the custom scale slider. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * Update flutter/lib/desktop/widgets/remote_toolbar.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * refactor(scale): centralize custom scale constants in consts.dart - Moved piecewise mapping constants for the custom scale slider from the remote toolbar to consts.dart for better organization and maintainability. - Introduced additional constants related to custom scale behavior, including minimum, pivot, and maximum percentages, as well as debounce duration. - Updated the remote toolbar to reference these centralized constants, improving code clarity and reducing duplication. These changes enhance the structure and readability of the custom scale implementation. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * refactor(consts): remove duplicate custom scale percent key definition - Eliminated redundant declaration of the custom scale percent key in consts.dart, ensuring a single source of truth for this constant. - This change improves code clarity and maintainability by reducing duplication. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * refactor(scale): update clamping logic to use centralized constants - Modified the clamping function to utilize the newly defined constants for minimum and maximum scale percentages, enhancing code maintainability and clarity. - This change ensures consistency across the application by referencing a single source for scale limits. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * Enhance RdoMenuButton behavior for custom scale selection - Updated the RdoMenuButton to include a new `closeOnActivate` parameter, allowing the submenu to remain open when selecting custom scale options. - Modified the onChanged callback to conditionally trigger a rebuild when entering custom mode, improving user experience by immediately displaying the slider controls. These changes streamline the interaction with the custom scale feature in the remote toolbar. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * refactor(toolbar): _DisplayMenuState to simplify scroll handling - Removed the _scrollEnabled state variable and its associated logic, streamlining the component's state management. - Updated the RdoMenuButton onChanged callbacks to directly reference the canvasModel's imageOverflow value, enhancing responsiveness and reducing complexity. These changes improve code clarity and maintainability in the remote toolbar's display menu. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * feat(lang): Add translations for custom scale features in multiple languages - Introduced new entries for "Scale custom", "Custom scale slider", "Decrease", and "Increase" in various language files to support the custom scale functionality. - This update enhances the localization of the application, ensuring users can interact with the custom scale features in their preferred language. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * feat(lang): Add translations for custom scale features in Catalan and Romanian - Updated language files for Catalan and Romanian to include translations for "Custom scale slider", "Decrease", and "Increase". - This enhancement improves the localization of the application, allowing users to interact with custom scale features in their native languages. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * fix(model): Correct error logging in getSessionCustomScale method - Updated the error logging statement in the getSessionCustomScale method to properly interpolate the exception message, improving debugging clarity. - This change ensures that error messages are more informative, aiding in troubleshooting issues related to session scaling. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * refactor(scale): Simplify clamping logic for custom scale percent - Updated the clampCustomScalePercent function to use the built-in clamp method, improving code readability and maintainability. - This change ensures consistent clamping behavior across the application by centralizing the logic for valid scale ranges. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * refactor(scale): Remove unused import for web bridge - Eliminated the conditional import of the web bridge from scale.dart, as it is no longer necessary. This change helps to clean up the code and improve maintainability by removing unused dependencies. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * chore(model): typo Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * Update flutter/lib/desktop/widgets/remote_toolbar.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore(toolbar): Clarify precision for scale adjustments in remote toolbar - Added comments to clarify the use of a wide range of divisions for the scale slider, allowing for ~1% precision increments. This change improves user experience by enabling more precise scale value settings, reducing the need for fine-tuning with +/- buttons. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * Update flutter/lib/models/model.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update flutter/lib/desktop/widgets/remote_toolbar.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(model): Enhance error logging in getSessionCustomScale method - Improved error logging by adding stack trace output to debugPrintStack, enhancing debugging capabilities for session scaling issues. - This change provides clearer insights into errors encountered during scale retrieval, aiding in troubleshooting. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * refactor(toolbar): Simplify custom scale percent retrieval in remote toolbar - Replaced the previous method of retrieving the custom scale percent with a new function, getSessionCustomScalePercent, enhancing code clarity and maintainability. - This change streamlines the process of obtaining the scale value, ensuring a more efficient and readable implementation. Signed-off-by: Alessandro De Blasis <alex@deblasis.net> * Update flutter/lib/desktop/widgets/remote_toolbar.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Signed-off-by: Alessandro De Blasis <alex@deblasis.net> Signed-off-by: Alessandro De Blasis alex@deblasis.net Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
982 lines
32 KiB
Dart
982 lines
32 KiB
Dart
import 'dart:async';
|
|
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;
|
|
final isDefaultConn = ffi.connType == ConnType.defaultConn;
|
|
|
|
List<TTextMenu> v = [];
|
|
// elevation
|
|
if (isDefaultConn &&
|
|
perms['keyboard'] != false &&
|
|
ffi.elevationModel.showRequestMenu) {
|
|
v.add(
|
|
TTextMenu(
|
|
child: Text(translate('Request Elevation')),
|
|
onPressed: () =>
|
|
showRequestElevationDialog(sessionId, ffi.dialogManager)),
|
|
);
|
|
}
|
|
// osAccount / osPassword
|
|
if (isDefaultConn && 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 (isDefaultConn &&
|
|
pi.platform != kPeerPlatformAndroid &&
|
|
perms['keyboard'] != false) {
|
|
v.add(TTextMenu(
|
|
child: Text(translate('Send clipboard keystrokes')),
|
|
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 (isDefaultConn && isMobile) {
|
|
v.add(TTextMenu(
|
|
child: Text(translate('Reset canvas')),
|
|
onPressed: () => ffi.cursorModel.reset()));
|
|
}
|
|
|
|
// https://github.com/rustdesk/rustdesk/pull/9731
|
|
// Does not work for connection established by "accept".
|
|
connectWithToken(
|
|
{bool isFileTransfer = false,
|
|
bool isViewCamera = false,
|
|
bool isTcpTunneling = false,
|
|
bool isTerminal = false}) {
|
|
final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
|
|
connect(context, id,
|
|
isFileTransfer: isFileTransfer,
|
|
isViewCamera: isViewCamera,
|
|
isTerminal: isTerminal,
|
|
isTcpTunneling: isTcpTunneling,
|
|
connToken: connToken);
|
|
}
|
|
|
|
if (isDefaultConn && isDesktop) {
|
|
v.add(
|
|
TTextMenu(
|
|
child: Text(translate('Transfer file')),
|
|
onPressed: () => connectWithToken(isFileTransfer: true)),
|
|
);
|
|
v.add(
|
|
TTextMenu(
|
|
child: Text(translate('View camera')),
|
|
onPressed: () => connectWithToken(isViewCamera: true)),
|
|
);
|
|
v.add(
|
|
TTextMenu(
|
|
child: Text('${translate('Terminal')} (beta)'),
|
|
onPressed: () => connectWithToken(isTerminal: true)),
|
|
);
|
|
v.add(
|
|
TTextMenu(
|
|
child: Text(translate('TCP tunneling')),
|
|
onPressed: () => connectWithToken(isTcpTunneling: true)),
|
|
);
|
|
}
|
|
// note
|
|
if (isDefaultConn &&
|
|
bind
|
|
.sessionGetAuditServerSync(sessionId: sessionId, typ: "conn")
|
|
.isNotEmpty) {
|
|
v.add(
|
|
TTextMenu(
|
|
child: Text(translate('Note')),
|
|
onPressed: () => showAuditDialog(ffi)),
|
|
);
|
|
}
|
|
// divider
|
|
if (isDefaultConn && (isDesktop || isWebDesktop)) {
|
|
v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true));
|
|
}
|
|
// ctrlAltDel
|
|
if (isDefaultConn &&
|
|
!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 (isDefaultConn &&
|
|
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 (isDefaultConn && !ffiModel.viewOnly && ffi.ffiModel.keyboard) {
|
|
v.add(
|
|
TTextMenu(
|
|
child: Text(translate('Insert Lock')),
|
|
onPressed: () => bind.sessionLockScreen(sessionId: sessionId)),
|
|
);
|
|
}
|
|
// blockUserInput
|
|
if (isDefaultConn &&
|
|
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 (isDefaultConn &&
|
|
isDesktop &&
|
|
ffiModel.keyboard &&
|
|
pi.platform != kPeerPlatformAndroid &&
|
|
pi.platform != kPeerPlatformMacOS &&
|
|
versionCmp(pi.version, '1.2.0') >= 0 &&
|
|
bind.peerGetSessionsCount(id: id, connType: ffi.connType.index) == 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()));
|
|
}
|
|
|
|
// to-do:
|
|
// 1. Web desktop
|
|
// 2. Mobile, copy the image to the clipboard
|
|
if (isDesktop) {
|
|
final isScreenshotSupported = bind.sessionGetCommonSync(
|
|
sessionId: sessionId, key: 'is_screenshot_supported', param: '');
|
|
if ('true' == isScreenshotSupported) {
|
|
v.add(TTextMenu(
|
|
child: Text(ffi.ffiModel.timerScreenshot != null
|
|
? '${translate('Taking screenshot')} ...'
|
|
: translate('Take screenshot')),
|
|
onPressed: ffi.ffiModel.timerScreenshot != null
|
|
? null
|
|
: () {
|
|
if (pi.currentDisplay == kAllDisplayValue) {
|
|
msgBox(
|
|
sessionId,
|
|
'custom-nook-nocancel-hasclose-info',
|
|
'Take screenshot',
|
|
'screenshot-merged-screen-not-supported-tip',
|
|
'',
|
|
ffi.dialogManager);
|
|
} else {
|
|
bind.sessionTakeScreenshot(
|
|
sessionId: sessionId, display: pi.currentDisplay);
|
|
ffi.ffiModel.timerScreenshot =
|
|
Timer(Duration(seconds: 30), () {
|
|
ffi.ffiModel.timerScreenshot = null;
|
|
});
|
|
}
|
|
},
|
|
));
|
|
}
|
|
}
|
|
// 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),
|
|
TRadioMenu<String>(
|
|
child: Text(translate('Scale custom')),
|
|
value: kRemoteViewStyleCustom,
|
|
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;
|
|
final isDefaultConn = ffi.connType == ConnType.defaultConn;
|
|
|
|
// 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 (isDefaultConn && 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 (isDefaultConn &&
|
|
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 (isDefaultConn && 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 (isDefaultConn && 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 (isDefaultConn && isMobile) {
|
|
v.addAll(toolbarKeyboardToggles(ffi));
|
|
}
|
|
|
|
// view mode (mobile only, desktop is in keyboard menu)
|
|
if (isDefaultConn && 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 [];
|
|
}
|