Merge pull request #6406 from fufesou/feat/virtual_display_privacy_mode

Feat/Windows - virtual display privacy mode
This commit is contained in:
RustDesk
2023-11-14 21:54:14 +08:00
committed by GitHub
69 changed files with 2283 additions and 982 deletions

View File

@@ -959,6 +959,7 @@ class CustomAlertDialog extends StatelessWidget {
void msgBox(SessionID sessionId, String type, String title, String text,
String link, OverlayDialogManager dialogManager,
{bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
dialogManager.dismissAll();
List<Widget> buttons = [];
bool hasOk = false;
@@ -1983,8 +1984,8 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
id = uri.authority;
}
if (isMobile){
if (id != null){
if (isMobile) {
if (id != null) {
connect(Get.context!, id);
return null;
}
@@ -2040,7 +2041,7 @@ connect(
final idController = Get.find<IDTextEditingController>();
idController.text = formatID(id);
}
if (Get.isRegistered<TextEditingController>()){
if (Get.isRegistered<TextEditingController>()) {
final fieldTextEditingController = Get.find<TextEditingController>();
fieldTextEditingController.text = formatID(id);
}

View File

@@ -11,7 +11,7 @@ class PrivacyModeState {
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
final RxBool state = false.obs;
final RxString state = ''.obs;
Get.put(state, tag: key);
}
}
@@ -21,11 +21,11 @@ class PrivacyModeState {
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
} else {
Get.find<RxBool>(tag: key).value = false;
Get.find<RxString>(tag: key).value = '';
}
}
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
static RxString find(String id) => Get.find<RxString>(tag: tag(id));
}
class BlockInputState {

View File

@@ -481,24 +481,6 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
},
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) ||
@@ -517,7 +499,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
if (useTextureRender &&
pi.isSupportMultiDisplay &&
PrivacyModeState.find(id).isFalse &&
PrivacyModeState.find(id).isEmpty &&
pi.displaysCount.value > 1 &&
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
final value =
@@ -567,3 +549,69 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
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) {
return TToggleMenu(
value: privacyModeState.isNotEmpty,
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;
}
final option = 'privacy-mode';
toggleFunc(sessionId, option);
},
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();
}
}

View File

@@ -23,6 +23,7 @@ const String kPlatformAdditionsHeadless = "headless";
const String kPlatformAdditionsIsInstalled = "is_installed";
const String kPlatformAdditionsVirtualDisplays = "virtual_displays";
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
const String kPlatformAdditionsSupportedPrivacyModeImpl = "supported_privacy_mode_impl";
const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux";

View File

@@ -1060,7 +1060,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
tmpWrapper() {
// Setting page is not modal, oldOptions should only be used when getting options, never when setting.
Map<String, dynamic> oldOptions =
jsonDecode(bind.mainGetOptionsSync() as String);
jsonDecode(bind.mainGetOptionsSync());
old(String key) {
return (oldOptions[key] ?? '').trim();
}
@@ -1151,6 +1151,7 @@ class _DisplayState extends State<_Display> {
scrollStyle(context),
imageQuality(context),
codec(context),
privacyModeImpl(context),
other(context),
]).marginOnly(bottom: _kListViewBottomMargin));
}
@@ -1290,6 +1291,42 @@ class _DisplayState extends State<_Display> {
]);
}
Widget privacyModeImpl(BuildContext context) {
final supportedPrivacyModeImpls = bind.mainSupportedPrivacyModeImpls();
late final List<dynamic> privacyModeImpls;
try {
privacyModeImpls = jsonDecode(supportedPrivacyModeImpls);
} catch (e) {
debugPrint('failed to parse supported privacy mode impls, err=$e');
return Offstage();
}
if (privacyModeImpls.length < 2) {
return Offstage();
}
final key = 'privacy-mode-impl-key';
onChanged(String value) async {
await bind.mainSetOption(key: key, value: value);
setState(() {});
}
String groupValue = bind.mainGetOptionSync(key: key);
if (groupValue.isEmpty) {
groupValue = bind.mainDefaultPrivacyModeImpl();
}
return _Card(
title: 'Privacy mode',
children: privacyModeImpls.map((impl) {
final d = impl as List<dynamic>;
return _Radio(context,
value: d[0] as String,
groupValue: groupValue,
label: d[1] as String,
onChanged: onChanged);
}).toList(),
);
}
Widget otherRow(String label, String key) {
final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
onChanged(bool b) async {

View File

@@ -17,6 +17,7 @@ import '../../common/widgets/overlay.dart';
import '../../common/widgets/remote_input.dart';
import '../../common.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/toolbar.dart';
import '../../models/model.dart';
import '../../models/desktop_render_texture.dart';
import '../../models/platform_model.dart';
@@ -281,24 +282,23 @@ class _RemotePageState extends State<RemotePage>
},
inputModel: _ffi.inputModel,
child: getBodyForDesktop(context))),
Stack(
children: [
_ffi.ffiModel.pi.isSet.isTrue &&
_ffi.ffiModel.waitForFirstImage.isTrue
? emptyOverlay()
: () {
_ffi.ffiModel.tryShowAndroidActionsOverlay();
return Offstage();
}(),
// Use Overlay to enable rebuild every time on menu button click.
_ffi.ffiModel.pi.isSet.isTrue
? Overlay(initialEntries: [
OverlayEntry(builder: remoteToolbar)
])
: remoteToolbar(context),
_ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
],
),
Stack(
children: [
_ffi.ffiModel.pi.isSet.isTrue &&
_ffi.ffiModel.waitForFirstImage.isTrue
? emptyOverlay()
: () {
_ffi.ffiModel.tryShowAndroidActionsOverlay();
return Offstage();
}(),
// Use Overlay to enable rebuild every time on menu button click.
_ffi.ffiModel.pi.isSet.isTrue
? Overlay(
initialEntries: [OverlayEntry(builder: remoteToolbar)])
: remoteToolbar(context),
_ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
],
),
],
);
}
@@ -309,12 +309,17 @@ class _RemotePageState extends State<RemotePage>
final imageReady = _ffi.ffiModel.pi.isSet.isTrue &&
_ffi.ffiModel.waitForFirstImage.isFalse;
if (imageReady) {
// `dismissAll()` is to ensure that the state is clean.
// It's ok to call dismissAll() here.
_ffi.dialogManager.dismissAll();
// Recreate the block state to refresh the state.
_blockableOverlayState = BlockableOverlayState();
_blockableOverlayState.applyFfi(_ffi);
// If the privacy mode(disable physical displays) is switched,
// we should not dismiss the dialog immediately.
if (DateTime.now().difference(togglePrivacyModeTime) >
const Duration(milliseconds: 3000)) {
// `dismissAll()` is to ensure that the state is clean.
// It's ok to call dismissAll() here.
_ffi.dialogManager.dismissAll();
// Recreate the block state to refresh the state.
_blockableOverlayState = BlockableOverlayState();
_blockableOverlayState.applyFfi(_ffi);
}
// Block the whole `bodyWidget()` when dialog shows.
return BlockableOverlay(
underlying: bodyWidget(),

View File

@@ -468,7 +468,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
}
toolbarItems.add(Obx(() {
if (PrivacyModeState.find(widget.id).isFalse &&
if (PrivacyModeState.find(widget.id).isEmpty &&
pi.displaysCount.value > 1) {
return _MonitorMenu(
id: widget.id,
@@ -1034,31 +1034,62 @@ class _DisplayMenuState extends State<_DisplayMenu> {
@override
Widget build(BuildContext context) {
_screenAdjustor.updateScreen();
return _IconSubmenuButton(
tooltip: 'Display Settings',
svg: "assets/display.svg",
final menuChildren = <Widget>[
_screenAdjustor.adjustWindow(context),
viewStyle(),
scrollStyle(),
imageQuality(),
codec(),
_ResolutionsMenu(
id: widget.id,
ffi: widget.ffi,
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
menuChildren: [
_screenAdjustor.adjustWindow(context),
viewStyle(),
scrollStyle(),
imageQuality(),
codec(),
_ResolutionsMenu(
id: widget.id,
ffi: widget.ffi,
screenAdjustor: _screenAdjustor,
),
_VirtualDisplayMenu(
id: widget.id,
ffi: widget.ffi,
),
screenAdjustor: _screenAdjustor,
),
_VirtualDisplayMenu(
id: widget.id,
ffi: widget.ffi,
),
Divider(),
toggles(),
];
// privacy mode
if (ffiModel.keyboard && pi.features.privacyMode) {
final privacyModeState = PrivacyModeState.find(id);
final privacyModeList =
toolbarPrivacyMode(privacyModeState, context, id, ffi);
if (privacyModeList.length == 1) {
menuChildren.add(CkbMenuButton(
value: privacyModeList[0].value,
onChanged: privacyModeList[0].onChanged,
child: privacyModeList[0].child,
ffi: ffi));
} else if (privacyModeList.length > 1) {
menuChildren.addAll([
Divider(),
toggles(),
widget.pluginItem,
_SubmenuButton(
ffi: widget.ffi,
child: Text(translate('Privacy Mode')),
menuChildren: privacyModeList
.map((e) => CkbMenuButton(
value: e.value,
onChanged: e.onChanged,
child: e.child,
ffi: ffi))
.toList()),
]);
}
}
menuChildren.add(widget.pluginItem);
return _IconSubmenuButton(
tooltip: 'Display Settings',
svg: "assets/display.svg",
ffi: widget.ffi,
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
menuChildren: menuChildren,
);
}
viewStyle() {
@@ -1495,32 +1526,39 @@ class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
}
final virtualDisplays = widget.ffi.ffiModel.pi.virtualDisplays;
final privacyModeState = PrivacyModeState.find(widget.id);
final children = <Widget>[];
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
children.add(CkbMenuButton(
value: virtualDisplays.contains(i + 1),
onChanged: (bool? value) async {
if (value != null) {
bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId, index: i + 1, on: value);
}
},
child: Text('${translate('Virtual display')} ${i + 1}'),
ffi: widget.ffi,
));
children.add(Obx(() => CkbMenuButton(
value: virtualDisplays.contains(i + 1),
onChanged: privacyModeState.isNotEmpty
? null
: (bool? value) async {
if (value != null) {
bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId,
index: i + 1,
on: value);
}
},
child: Text('${translate('Virtual display')} ${i + 1}'),
ffi: widget.ffi,
)));
}
children.add(Divider());
children.add(MenuButton(
onPressed: () {
bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
},
ffi: widget.ffi,
child: Text(translate('Plug out all')),
));
children.add(Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
},
ffi: widget.ffi,
child: Text(translate('Plug out all')),
)));
return _SubmenuButton(
ffi: widget.ffi,
menuChildren: children,

View File

@@ -21,6 +21,7 @@ import '../../models/input_model.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../utils/image.dart';
import '../widgets/dialog.dart';
final initText = '1' * 1024;
@@ -807,6 +808,16 @@ void showOptions(
List<TToggleMenu> displayToggles =
await toolbarDisplayToggle(context, id, gFFI);
List<TToggleMenu> privacyModeList = [];
// privacy mode
final privacyModeState = PrivacyModeState.find(id);
if (gFFI.ffiModel.keyboard && gFFI.ffiModel.pi.features.privacyMode) {
privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI);
if (privacyModeList.length == 1) {
displayToggles.add(privacyModeList[0]);
}
}
dialogManager.show((setState, close, context) {
var viewStyle =
(viewStyleRadios.isNotEmpty ? viewStyleRadios[0].groupValue : '').obs;
@@ -849,10 +860,21 @@ void showOptions(
title: e.value.child)))
.toList();
Widget privacyModeWidget = Offstage();
if (privacyModeList.length > 1) {
privacyModeWidget = ListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
title: Text(translate('Privacy mode')),
onTap: () => setPrivacyModeDialog(
dialogManager, privacyModeList, privacyModeState),
);
}
return CustomAlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: displays + radios + toggles),
children: displays + radios + toggles + [privacyModeWidget]),
);
}, clickMaskDismiss: true, backDismiss: true);
}

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/common/widgets/toolbar.dart';
import 'package:get/get.dart';
import '../../common.dart';
@@ -259,6 +260,30 @@ void showServerSettingsWithValue(
});
}
void setPrivacyModeDialog(
OverlayDialogManager dialogManager,
List<TToggleMenu> privacyModeList,
RxString privacyModeState,
) async {
dialogManager.dismissAll();
dialogManager.show((setState, close, context) {
return CustomAlertDialog(
title: Text(translate('Privacy mode')),
content: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: privacyModeList
.map((value) => CheckboxListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
title: value.child,
value: value.value,
onChanged: value.onChanged,
))
.toList()),
);
}, backDismiss: true, clickMaskDismiss: true);
}
Future<String?> validateAsync(String value) async {
value = value.trim();
if (value.isEmpty) {

View File

@@ -967,11 +967,21 @@ class FfiModel with ChangeNotifier {
}
updatePrivacyMode(
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
Map<String, dynamic> evt, SessionID sessionId, String peerId) async {
notifyListeners();
try {
PrivacyModeState.find(peerId).value = bind.sessionGetToggleOptionSync(
final isOn = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: 'privacy-mode');
if (isOn) {
var privacyModeImpl = await bind.sessionGetOption(
sessionId: sessionId, arg: 'privacy-mode-impl-key');
// For compatibility, version < 1.2.4, the default value is 'privacy_mode_impl_mag'.
final initDefaultPrivacyMode = 'privacy_mode_impl_mag';
PrivacyModeState.find(peerId).value =
privacyModeImpl ?? initDefaultPrivacyMode;
} else {
PrivacyModeState.find(peerId).value = '';
}
} catch (e) {
//
}