mirror of
https://github.com/rustdesk/rustdesk.git
synced 2025-06-11 12:43:12 +08:00
merge mobile/desktop remote toobar code
Signed-off-by: 21pages <pages21@163.com>
This commit is contained in:
parent
af01581abb
commit
b2c0590898
@ -43,6 +43,7 @@ final isIOS = Platform.isIOS;
|
|||||||
final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||||
var isWeb = false;
|
var isWeb = false;
|
||||||
var isWebDesktop = false;
|
var isWebDesktop = false;
|
||||||
|
var isMobile = isAndroid || isIOS;
|
||||||
var version = "";
|
var version = "";
|
||||||
int androidVersion = 0;
|
int androidVersion = 0;
|
||||||
|
|
||||||
@ -1158,40 +1159,19 @@ class AndroidPermissionManager {
|
|||||||
// Used only for mobile, pages remote, settings, dialog
|
// Used only for mobile, pages remote, settings, dialog
|
||||||
// TODO remove argument contentPadding, it’s not used, getToggle() has not
|
// TODO remove argument contentPadding, it’s not used, getToggle() has not
|
||||||
RadioListTile<T> getRadio<T>(
|
RadioListTile<T> getRadio<T>(
|
||||||
String name, T toValue, T curValue, void Function(T?) onChange,
|
Widget title, T toValue, T curValue, ValueChanged<T?>? onChange,
|
||||||
{EdgeInsetsGeometry? contentPadding}) {
|
{EdgeInsetsGeometry? contentPadding}) {
|
||||||
return RadioListTile<T>(
|
return RadioListTile<T>(
|
||||||
contentPadding: contentPadding ?? EdgeInsets.zero,
|
contentPadding: contentPadding ?? EdgeInsets.zero,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
controlAffinity: ListTileControlAffinity.trailing,
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
title: Text(translate(name)),
|
title: title,
|
||||||
value: toValue,
|
value: toValue,
|
||||||
groupValue: curValue,
|
groupValue: curValue,
|
||||||
onChanged: onChange,
|
onChanged: onChange,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO move this to mobile/widgets.
|
|
||||||
// Used only for mobile, pages remote, settings, dialog
|
|
||||||
CheckboxListTile getToggle(
|
|
||||||
String id, void Function(void Function()) setState, option, name,
|
|
||||||
{FFI? ffi}) {
|
|
||||||
final opt = bind.sessionGetToggleOptionSync(id: id, arg: option);
|
|
||||||
return CheckboxListTile(
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
value: opt,
|
|
||||||
onChanged: (v) {
|
|
||||||
setState(() {
|
|
||||||
bind.sessionToggleOption(id: id, value: option);
|
|
||||||
});
|
|
||||||
if (option == "show-quality-monitor") {
|
|
||||||
(ffi ?? gFFI).qualityMonitorModel.checkShowQualityMonitor(id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title: Text(translate(name)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// find ffi, tag is Remote ID
|
/// find ffi, tag is Remote ID
|
||||||
/// for session specific usage
|
/// for session specific usage
|
||||||
FFI ffi(String? tag) {
|
FFI ffi(String? tag) {
|
||||||
|
@ -261,3 +261,23 @@ class PeerStringOption {
|
|||||||
static RxString find(String id, String opt) =>
|
static RxString find(String id, String opt) =>
|
||||||
Get.find<RxString>(tag: tag(id, opt));
|
Get.find<RxString>(tag: tag(id, opt));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initSharedStates(String id) {
|
||||||
|
PrivacyModeState.init(id);
|
||||||
|
BlockInputState.init(id);
|
||||||
|
CurrentDisplayState.init(id);
|
||||||
|
KeyboardEnabledState.init(id);
|
||||||
|
ShowRemoteCursorState.init(id);
|
||||||
|
RemoteCursorMovedState.init(id);
|
||||||
|
PeerBoolOption.init(id, 'zoom-cursor', () => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSharedStates(String id) {
|
||||||
|
PrivacyModeState.delete(id);
|
||||||
|
BlockInputState.delete(id);
|
||||||
|
CurrentDisplayState.delete(id);
|
||||||
|
ShowRemoteCursorState.delete(id);
|
||||||
|
KeyboardEnabledState.delete(id);
|
||||||
|
RemoteCursorMovedState.delete(id);
|
||||||
|
PeerBoolOption.delete(id, 'zoom-cursor');
|
||||||
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:debounce_throttle/debounce_throttle.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/common/shared_state.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
@ -879,7 +881,9 @@ void showRestartRemoteDevice(
|
|||||||
await dialogManager.show<bool>((setState, close) => CustomAlertDialog(
|
await dialogManager.show<bool>((setState, close) => CustomAlertDialog(
|
||||||
title: Row(children: [
|
title: Row(children: [
|
||||||
Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28),
|
Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28),
|
||||||
Text(translate("Restart Remote Device")).paddingOnly(left: 10),
|
Flexible(
|
||||||
|
child: Text(translate("Restart Remote Device"))
|
||||||
|
.paddingOnly(left: 10)),
|
||||||
]),
|
]),
|
||||||
content: Text(
|
content: Text(
|
||||||
"${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"),
|
"${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"),
|
||||||
@ -1047,3 +1051,221 @@ showSetOSAccount(
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showAuditDialog(String id, dialogManager) async {
|
||||||
|
final controller = TextEditingController();
|
||||||
|
dialogManager.show((setState, close) {
|
||||||
|
submit() {
|
||||||
|
var text = controller.text.trim();
|
||||||
|
if (text != '') {
|
||||||
|
bind.sessionSendNote(id: id, note: text);
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
late final focusNode = FocusNode(
|
||||||
|
onKey: (FocusNode node, RawKeyEvent evt) {
|
||||||
|
if (evt.logicalKey.keyLabel == 'Enter') {
|
||||||
|
if (evt is RawKeyDownEvent) {
|
||||||
|
int pos = controller.selection.base.offset;
|
||||||
|
controller.text =
|
||||||
|
'${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}';
|
||||||
|
controller.selection =
|
||||||
|
TextSelection.fromPosition(TextPosition(offset: pos + 1));
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
if (evt.logicalKey.keyLabel == 'Esc') {
|
||||||
|
if (evt is RawKeyDownEvent) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
} else {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate('Note')),
|
||||||
|
content: SizedBox(
|
||||||
|
width: 250,
|
||||||
|
height: 120,
|
||||||
|
child: TextField(
|
||||||
|
autofocus: true,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
textInputAction: TextInputAction.newline,
|
||||||
|
decoration: const InputDecoration.collapsed(
|
||||||
|
hintText: 'input note here',
|
||||||
|
),
|
||||||
|
maxLines: null,
|
||||||
|
maxLength: 256,
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
)),
|
||||||
|
actions: [
|
||||||
|
dialogButton('Cancel', onPressed: close, isOutline: true),
|
||||||
|
dialogButton('OK', onPressed: submit)
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void showConfirmSwitchSidesDialog(
|
||||||
|
String id, OverlayDialogManager dialogManager) async {
|
||||||
|
dialogManager.show((setState, close) {
|
||||||
|
submit() async {
|
||||||
|
await bind.sessionSwitchSides(id: id);
|
||||||
|
closeConnection(id: id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
content: msgboxContent('info', 'Switch Sides',
|
||||||
|
'Please confirm if you want to share your desktop?'),
|
||||||
|
actions: [
|
||||||
|
dialogButton('Cancel', onPressed: close, isOutline: true),
|
||||||
|
dialogButton('OK', onPressed: submit),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
customImageQualityDialog(String id, FFI ffi) async {
|
||||||
|
double qualityInitValue = 50;
|
||||||
|
double fpsInitValue = 30;
|
||||||
|
bool qualitySet = false;
|
||||||
|
bool fpsSet = false;
|
||||||
|
setCustomValues({double? quality, double? fps}) async {
|
||||||
|
if (quality != null) {
|
||||||
|
qualitySet = true;
|
||||||
|
await bind.sessionSetCustomImageQuality(id: id, value: quality.toInt());
|
||||||
|
}
|
||||||
|
if (fps != null) {
|
||||||
|
fpsSet = true;
|
||||||
|
await bind.sessionSetCustomFps(id: id, fps: fps.toInt());
|
||||||
|
}
|
||||||
|
if (!qualitySet) {
|
||||||
|
qualitySet = true;
|
||||||
|
await bind.sessionSetCustomImageQuality(
|
||||||
|
id: id, value: qualityInitValue.toInt());
|
||||||
|
}
|
||||||
|
if (!fpsSet) {
|
||||||
|
fpsSet = true;
|
||||||
|
await bind.sessionSetCustomFps(id: id, fps: fpsInitValue.toInt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final btnClose = dialogButton('Close', onPressed: () async {
|
||||||
|
await setCustomValues();
|
||||||
|
ffi.dialogManager.dismissAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
// quality
|
||||||
|
final quality = await bind.sessionGetCustomImageQuality(id: id);
|
||||||
|
qualityInitValue =
|
||||||
|
quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0;
|
||||||
|
const qualityMinValue = 10.0;
|
||||||
|
const qualityMaxValue = 100.0;
|
||||||
|
if (qualityInitValue < qualityMinValue) {
|
||||||
|
qualityInitValue = qualityMinValue;
|
||||||
|
}
|
||||||
|
if (qualityInitValue > qualityMaxValue) {
|
||||||
|
qualityInitValue = qualityMaxValue;
|
||||||
|
}
|
||||||
|
final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
|
||||||
|
final debouncerQuality = Debouncer<double>(
|
||||||
|
Duration(milliseconds: 1000),
|
||||||
|
onChanged: (double v) {
|
||||||
|
setCustomValues(quality: v);
|
||||||
|
},
|
||||||
|
initialValue: qualityInitValue,
|
||||||
|
);
|
||||||
|
final qualitySlider = Obx(() => Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: Slider(
|
||||||
|
value: qualitySliderValue.value,
|
||||||
|
min: qualityMinValue,
|
||||||
|
max: qualityMaxValue,
|
||||||
|
divisions: 18,
|
||||||
|
onChanged: (double value) {
|
||||||
|
qualitySliderValue.value = value;
|
||||||
|
debouncerQuality.value = value;
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Text(
|
||||||
|
'${qualitySliderValue.value.round()}%',
|
||||||
|
style: const TextStyle(fontSize: 15),
|
||||||
|
)),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Text(
|
||||||
|
translate('Bitrate'),
|
||||||
|
style: const TextStyle(fontSize: 15),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
// fps
|
||||||
|
final fpsOption = await bind.sessionGetOption(id: id, arg: 'custom-fps');
|
||||||
|
fpsInitValue = fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30;
|
||||||
|
if (fpsInitValue < 5 || fpsInitValue > 120) {
|
||||||
|
fpsInitValue = 30;
|
||||||
|
}
|
||||||
|
final RxDouble fpsSliderValue = RxDouble(fpsInitValue);
|
||||||
|
final debouncerFps = Debouncer<double>(
|
||||||
|
Duration(milliseconds: 1000),
|
||||||
|
onChanged: (double v) {
|
||||||
|
setCustomValues(fps: v);
|
||||||
|
},
|
||||||
|
initialValue: qualityInitValue,
|
||||||
|
);
|
||||||
|
bool? direct;
|
||||||
|
try {
|
||||||
|
direct =
|
||||||
|
ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect;
|
||||||
|
} catch (_) {}
|
||||||
|
final fpsSlider = Offstage(
|
||||||
|
offstage: (await bind.mainIsUsingPublicServer() && direct != true) ||
|
||||||
|
version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: Obx((() => Slider(
|
||||||
|
value: fpsSliderValue.value,
|
||||||
|
min: 5,
|
||||||
|
max: 120,
|
||||||
|
divisions: 23,
|
||||||
|
onChanged: (double value) {
|
||||||
|
fpsSliderValue.value = value;
|
||||||
|
debouncerFps.value = value;
|
||||||
|
},
|
||||||
|
)))),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Obx(() => Text(
|
||||||
|
'${fpsSliderValue.value.round()}',
|
||||||
|
style: const TextStyle(fontSize: 15),
|
||||||
|
))),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Text(
|
||||||
|
translate('FPS'),
|
||||||
|
style: const TextStyle(fontSize: 15),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final content = Column(
|
||||||
|
children: [qualitySlider, fpsSlider],
|
||||||
|
);
|
||||||
|
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
|
||||||
|
}
|
||||||
|
449
flutter/lib/common/widgets/toolbar.dart
Normal file
449
flutter/lib/common/widgets/toolbar.dart
Normal file
@ -0,0 +1,449 @@
|
|||||||
|
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:get/get.dart';
|
||||||
|
|
||||||
|
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});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||||
|
final ffiModel = ffi.ffiModel;
|
||||||
|
final pi = ffiModel.pi;
|
||||||
|
final perms = ffiModel.permissions;
|
||||||
|
|
||||||
|
List<TTextMenu> v = [];
|
||||||
|
// elevation
|
||||||
|
if (ffi.elevationModel.showRequestMenu) {
|
||||||
|
v.add(
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Request Elevation')),
|
||||||
|
onPressed: () => showRequestElevationDialog(id, ffi.dialogManager)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// osAccount / osPassword
|
||||||
|
v.add(
|
||||||
|
TTextMenu(
|
||||||
|
child: Row(children: [
|
||||||
|
Text(translate(pi.is_headless ? 'OS Account' : 'OS Password')),
|
||||||
|
Offstage(
|
||||||
|
offstage: isDesktop,
|
||||||
|
child:
|
||||||
|
Icon(Icons.edit, color: MyTheme.accent).marginOnly(left: 12))
|
||||||
|
]),
|
||||||
|
trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)),
|
||||||
|
onPressed: () => pi.is_headless
|
||||||
|
? showSetOSAccount(id, ffi.dialogManager)
|
||||||
|
: showSetOSPassword(id, false, 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(id: id, 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(id: id, typ: "conn").isNotEmpty) {
|
||||||
|
v.add(
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Note')),
|
||||||
|
onPressed: () => showAuditDialog(id, ffi.dialogManager)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 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(id: id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 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, ffi.dialogManager)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// insertLock
|
||||||
|
if (!ffiModel.viewOnly && ffi.ffiModel.keyboard) {
|
||||||
|
v.add(
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Insert Lock')),
|
||||||
|
onPressed: () => bind.sessionLockScreen(id: id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 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(
|
||||||
|
id: id, value: '${blockInput.value ? 'un' : ''}block-input');
|
||||||
|
blockInput.value = !blockInput.value;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// switchSides
|
||||||
|
if (isDesktop &&
|
||||||
|
ffiModel.keyboard &&
|
||||||
|
pi.platform != kPeerPlatformAndroid &&
|
||||||
|
pi.platform != kPeerPlatformMacOS &&
|
||||||
|
version_cmp(pi.version, '1.2.0') >= 0) {
|
||||||
|
v.add(TTextMenu(
|
||||||
|
child: Text(translate('Switch Sides')),
|
||||||
|
onPressed: () => showConfirmSwitchSidesDialog(id, ffi.dialogManager)));
|
||||||
|
}
|
||||||
|
// refresh
|
||||||
|
if (pi.version.isNotEmpty) {
|
||||||
|
v.add(TTextMenu(
|
||||||
|
child: Text(translate('Refresh')),
|
||||||
|
onPressed: () => bind.sessionRefresh(id: id)));
|
||||||
|
}
|
||||||
|
// 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()));
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<TRadioMenu<String>>> toolbarViewStyle(
|
||||||
|
BuildContext context, String id, FFI ffi) async {
|
||||||
|
final groupValue = await bind.sessionGetViewStyle(id: id) ?? '';
|
||||||
|
void onChanged(String? value) async {
|
||||||
|
if (value == null) return;
|
||||||
|
bind
|
||||||
|
.sessionSetViewStyle(id: id, 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(id: id) ?? '';
|
||||||
|
onChanged(String? value) async {
|
||||||
|
if (value == null) return;
|
||||||
|
await bind.sessionSetImageQuality(id: id, 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(id, ffi);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<TRadioMenu<String>>> toolbarCodec(
|
||||||
|
BuildContext context, String id, FFI ffi) async {
|
||||||
|
final alternativeCodecs = await bind.sessionAlternativeCodecs(id: id);
|
||||||
|
final groupValue =
|
||||||
|
await bind.sessionGetOption(id: id, arg: 'codec-preference') ?? '';
|
||||||
|
final List<bool> codecs = [];
|
||||||
|
try {
|
||||||
|
final Map codecsJson = jsonDecode(alternativeCodecs);
|
||||||
|
final vp8 = codecsJson['vp8'] ?? false;
|
||||||
|
final h264 = codecsJson['h264'] ?? false;
|
||||||
|
final h265 = codecsJson['h265'] ?? false;
|
||||||
|
codecs.add(vp8);
|
||||||
|
codecs.add(h264);
|
||||||
|
codecs.add(h265);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Show Codec Preference err=$e");
|
||||||
|
}
|
||||||
|
final visible = codecs.length == 3 && (codecs[0] || codecs[1] || codecs[2]);
|
||||||
|
if (!visible) return [];
|
||||||
|
onChanged(String? value) async {
|
||||||
|
if (value == null) return;
|
||||||
|
await bind.sessionPeerOption(
|
||||||
|
id: id, name: 'codec-preference', value: value);
|
||||||
|
bind.sessionChangePreferCodec(id: id);
|
||||||
|
}
|
||||||
|
|
||||||
|
TRadioMenu<String> radio(String label, String value, bool enabled) {
|
||||||
|
return TRadioMenu<String>(
|
||||||
|
child: Text(translate(label)),
|
||||||
|
value: value,
|
||||||
|
groupValue: groupValue,
|
||||||
|
onChanged: enabled ? onChanged : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
radio('Auto', 'auto', true),
|
||||||
|
if (isDesktop || codecs[0]) radio('VP8', 'vp8', codecs[0]),
|
||||||
|
radio('VP9', 'vp9', true),
|
||||||
|
if (isDesktop || codecs[1]) radio('H264', 'h264', codecs[1]),
|
||||||
|
if (isDesktop || codecs[2]) radio('H265', 'h265', codecs[2]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// show remote cursor
|
||||||
|
if (pi.platform != kPeerPlatformAndroid &&
|
||||||
|
!ffi.canvasModel.cursorEmbedded &&
|
||||||
|
!pi.is_wayland) {
|
||||||
|
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(id: id, value: option);
|
||||||
|
state.value =
|
||||||
|
bind.sessionGetToggleOptionSync(id: id, arg: option);
|
||||||
|
}
|
||||||
|
: null));
|
||||||
|
}
|
||||||
|
// zoom cursor
|
||||||
|
final viewStyle = await bind.sessionGetViewStyle(id: id) ?? '';
|
||||||
|
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(id: id, value: option);
|
||||||
|
peerState.value = bind.sessionGetToggleOptionSync(id: id, arg: option);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// show quality monitor
|
||||||
|
final option = 'show-quality-monitor';
|
||||||
|
v.add(TToggleMenu(
|
||||||
|
value: bind.sessionGetToggleOptionSync(id: id, arg: option),
|
||||||
|
onChanged: (value) async {
|
||||||
|
if (value == null) return;
|
||||||
|
await bind.sessionToggleOption(id: id, value: option);
|
||||||
|
ffi.qualityMonitorModel.checkShowQualityMonitor(id);
|
||||||
|
},
|
||||||
|
child: Text(translate('Show quality monitor'))));
|
||||||
|
// mute
|
||||||
|
if (perms['audio'] != false) {
|
||||||
|
final option = 'disable-audio';
|
||||||
|
final value = bind.sessionGetToggleOptionSync(id: id, arg: option);
|
||||||
|
v.add(TToggleMenu(
|
||||||
|
value: value,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
bind.sessionToggleOption(id: id, 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(id: id, arg: option);
|
||||||
|
v.add(TToggleMenu(
|
||||||
|
value: value,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
bind.sessionToggleOption(id: id, 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(id: id, arg: option);
|
||||||
|
if (ffiModel.viewOnly) value = true;
|
||||||
|
v.add(TToggleMenu(
|
||||||
|
value: value,
|
||||||
|
onChanged: enabled
|
||||||
|
? (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
bind.sessionToggleOption(id: id, 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(id: id, arg: option);
|
||||||
|
v.add(TToggleMenu(
|
||||||
|
value: value,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
bind.sessionToggleOption(id: id, 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) {
|
||||||
|
msgBox(id, 'custom-nook-nocancel-hasclose', 'info',
|
||||||
|
'Please switch to Display 1 first', '', ffi.dialogManager);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bind.sessionToggleOption(id: id, 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(id: id, arg: option);
|
||||||
|
v.add(TToggleMenu(
|
||||||
|
value: value,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
bind.sessionToggleOption(id: id, value: option);
|
||||||
|
},
|
||||||
|
child: Text(translate('Swap control-command key'))));
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
@ -77,15 +77,8 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
late FFI _ffi;
|
late FFI _ffi;
|
||||||
|
|
||||||
void _initStates(String id) {
|
void _initStates(String id) {
|
||||||
PrivacyModeState.init(id);
|
initSharedStates(id);
|
||||||
BlockInputState.init(id);
|
_zoomCursor = PeerBoolOption.find(id, 'zoom-cursor');
|
||||||
CurrentDisplayState.init(id);
|
|
||||||
KeyboardEnabledState.init(id);
|
|
||||||
ShowRemoteCursorState.init(id);
|
|
||||||
RemoteCursorMovedState.init(id);
|
|
||||||
final optZoomCursor = 'zoom-cursor';
|
|
||||||
PeerBoolOption.init(id, optZoomCursor, () => false);
|
|
||||||
_zoomCursor = PeerBoolOption.find(id, optZoomCursor);
|
|
||||||
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
||||||
_keyboardEnabled = KeyboardEnabledState.find(id);
|
_keyboardEnabled = KeyboardEnabledState.find(id);
|
||||||
_remoteCursorMoved = RemoteCursorMovedState.find(id);
|
_remoteCursorMoved = RemoteCursorMovedState.find(id);
|
||||||
@ -93,15 +86,6 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_textureId = RxInt(-1);
|
_textureId = RxInt(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _removeStates(String id) {
|
|
||||||
PrivacyModeState.delete(id);
|
|
||||||
BlockInputState.delete(id);
|
|
||||||
CurrentDisplayState.delete(id);
|
|
||||||
ShowRemoteCursorState.delete(id);
|
|
||||||
KeyboardEnabledState.delete(id);
|
|
||||||
RemoteCursorMovedState.delete(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -217,7 +201,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
}
|
}
|
||||||
Get.delete<FFI>(tag: widget.id);
|
Get.delete<FFI>(tag: widget.id);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
_removeStates(widget.id);
|
removeSharedStates(widget.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildBody(BuildContext context) {
|
Widget buildBody(BuildContext context) {
|
||||||
|
@ -4,6 +4,7 @@ import 'dart:ui' as ui;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
@ -31,7 +32,6 @@ class MenubarState {
|
|||||||
final kStoreKey = 'remoteMenubarState';
|
final kStoreKey = 'remoteMenubarState';
|
||||||
late RxBool show;
|
late RxBool show;
|
||||||
late RxBool _pin;
|
late RxBool _pin;
|
||||||
RxString viewStyle = RxString(kRemoteViewStyleOriginal);
|
|
||||||
|
|
||||||
MenubarState() {
|
MenubarState() {
|
||||||
final s = bind.getLocalFlutterConfig(k: kStoreKey);
|
final s = bind.getLocalFlutterConfig(k: kStoreKey);
|
||||||
@ -456,7 +456,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
return Theme.of(context).copyWith(
|
return Theme.of(context).copyWith(
|
||||||
menuButtonTheme: MenuButtonThemeData(
|
menuButtonTheme: MenuButtonThemeData(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
minimumSize: MaterialStatePropertyAll(Size(64, 36)),
|
minimumSize: MaterialStatePropertyAll(Size(64, 32)),
|
||||||
textStyle: MaterialStatePropertyAll(
|
textStyle: MaterialStatePropertyAll(
|
||||||
TextStyle(fontWeight: FontWeight.normal),
|
TextStyle(fontWeight: FontWeight.normal),
|
||||||
),
|
),
|
||||||
@ -637,229 +637,17 @@ class _ControlMenu extends StatelessWidget {
|
|||||||
color: _MenubarTheme.blueColor,
|
color: _MenubarTheme.blueColor,
|
||||||
hoverColor: _MenubarTheme.hoverBlueColor,
|
hoverColor: _MenubarTheme.hoverBlueColor,
|
||||||
ffi: ffi,
|
ffi: ffi,
|
||||||
menuChildren: [
|
menuChildren: toolbarControls(context, id, ffi).map((e) {
|
||||||
requestElevation(),
|
if (e.divider) {
|
||||||
ffi.ffiModel.pi.is_headless ? osAccount() : osPassword(),
|
return Divider();
|
||||||
transferFile(context),
|
|
||||||
tcpTunneling(context),
|
|
||||||
note(),
|
|
||||||
Divider(),
|
|
||||||
ctrlAltDel(),
|
|
||||||
restart(),
|
|
||||||
insertLock(),
|
|
||||||
blockUserInput(),
|
|
||||||
switchSides(),
|
|
||||||
refresh(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
requestElevation() {
|
|
||||||
final visible = ffi.elevationModel.showRequestMenu;
|
|
||||||
if (!visible) return Offstage();
|
|
||||||
return _MenuItemButton(
|
|
||||||
child: Text(translate('Request Elevation')),
|
|
||||||
ffi: ffi,
|
|
||||||
onPressed: () => showRequestElevationDialog(id, ffi.dialogManager));
|
|
||||||
}
|
|
||||||
|
|
||||||
osAccount() {
|
|
||||||
return _MenuItemButton(
|
|
||||||
child: Text(translate('OS Account')),
|
|
||||||
trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)),
|
|
||||||
ffi: ffi,
|
|
||||||
onPressed: () => showSetOSAccount(id, ffi.dialogManager));
|
|
||||||
}
|
|
||||||
|
|
||||||
osPassword() {
|
|
||||||
return _MenuItemButton(
|
|
||||||
child: Text(translate('OS Password')),
|
|
||||||
trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)),
|
|
||||||
ffi: ffi,
|
|
||||||
onPressed: () => showSetOSPassword(id, false, ffi.dialogManager));
|
|
||||||
}
|
|
||||||
|
|
||||||
transferFile(BuildContext context) {
|
|
||||||
return _MenuItemButton(
|
|
||||||
child: Text(translate('Transfer File')),
|
|
||||||
ffi: ffi,
|
|
||||||
onPressed: () => connect(context, id, isFileTransfer: true));
|
|
||||||
}
|
|
||||||
|
|
||||||
tcpTunneling(BuildContext context) {
|
|
||||||
return _MenuItemButton(
|
|
||||||
child: Text(translate('TCP Tunneling')),
|
|
||||||
ffi: ffi,
|
|
||||||
onPressed: () => connect(context, id, isTcpTunneling: true));
|
|
||||||
}
|
|
||||||
|
|
||||||
note() {
|
|
||||||
final auditServer = bind.sessionGetAuditServerSync(id: id, typ: "conn");
|
|
||||||
final visible = auditServer.isNotEmpty;
|
|
||||||
if (!visible) return Offstage();
|
|
||||||
return _MenuItemButton(
|
|
||||||
child: Text(translate('Note')),
|
|
||||||
ffi: ffi,
|
|
||||||
onPressed: () => _showAuditDialog(id, ffi.dialogManager),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_showAuditDialog(String id, dialogManager) async {
|
|
||||||
final controller = TextEditingController();
|
|
||||||
dialogManager.show((setState, close) {
|
|
||||||
submit() {
|
|
||||||
var text = controller.text.trim();
|
|
||||||
if (text != '') {
|
|
||||||
bind.sessionSendNote(id: id, note: text);
|
|
||||||
}
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
late final focusNode = FocusNode(
|
|
||||||
onKey: (FocusNode node, RawKeyEvent evt) {
|
|
||||||
if (evt.logicalKey.keyLabel == 'Enter') {
|
|
||||||
if (evt is RawKeyDownEvent) {
|
|
||||||
int pos = controller.selection.base.offset;
|
|
||||||
controller.text =
|
|
||||||
'${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}';
|
|
||||||
controller.selection =
|
|
||||||
TextSelection.fromPosition(TextPosition(offset: pos + 1));
|
|
||||||
}
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
}
|
|
||||||
if (evt.logicalKey.keyLabel == 'Esc') {
|
|
||||||
if (evt is RawKeyDownEvent) {
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
} else {
|
} else {
|
||||||
return KeyEventResult.ignored;
|
return _MenuItemButton(
|
||||||
|
child: e.child,
|
||||||
|
onPressed: e.onPressed,
|
||||||
|
ffi: ffi,
|
||||||
|
trailingIcon: e.trailingIcon);
|
||||||
}
|
}
|
||||||
},
|
}).toList());
|
||||||
);
|
|
||||||
|
|
||||||
return CustomAlertDialog(
|
|
||||||
title: Text(translate('Note')),
|
|
||||||
content: SizedBox(
|
|
||||||
width: 250,
|
|
||||||
height: 120,
|
|
||||||
child: TextField(
|
|
||||||
autofocus: true,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
textInputAction: TextInputAction.newline,
|
|
||||||
decoration: const InputDecoration.collapsed(
|
|
||||||
hintText: 'input note here',
|
|
||||||
),
|
|
||||||
maxLines: null,
|
|
||||||
maxLength: 256,
|
|
||||||
controller: controller,
|
|
||||||
focusNode: focusNode,
|
|
||||||
)),
|
|
||||||
actions: [
|
|
||||||
dialogButton('Cancel', onPressed: close, isOutline: true),
|
|
||||||
dialogButton('OK', onPressed: submit)
|
|
||||||
],
|
|
||||||
onSubmit: submit,
|
|
||||||
onCancel: close,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrlAltDel() {
|
|
||||||
final viewOnly = ffi.ffiModel.viewOnly;
|
|
||||||
final pi = ffi.ffiModel.pi;
|
|
||||||
final visible = !viewOnly &&
|
|
||||||
ffi.ffiModel.keyboard &&
|
|
||||||
(pi.platform == kPeerPlatformLinux || pi.sasEnabled);
|
|
||||||
if (!visible) return Offstage();
|
|
||||||
return _MenuItemButton(
|
|
||||||
child: Text('${translate("Insert")} Ctrl + Alt + Del'),
|
|
||||||
ffi: ffi,
|
|
||||||
onPressed: () => bind.sessionCtrlAltDel(id: id));
|
|
||||||
}
|
|
||||||
|
|
||||||
restart() {
|
|
||||||
final perms = ffi.ffiModel.permissions;
|
|
||||||
final pi = ffi.ffiModel.pi;
|
|
||||||
final visible = perms['restart'] != false &&
|
|
||||||
(pi.platform == kPeerPlatformLinux ||
|
|
||||||
pi.platform == kPeerPlatformWindows ||
|
|
||||||
pi.platform == kPeerPlatformMacOS);
|
|
||||||
if (!visible) return Offstage();
|
|
||||||
return _MenuItemButton(
|
|
||||||
child: Text(translate('Restart Remote Device')),
|
|
||||||
ffi: ffi,
|
|
||||||
onPressed: () => showRestartRemoteDevice(pi, id, ffi.dialogManager));
|
|
||||||
}
|
|
||||||
|
|
||||||
insertLock() {
|
|
||||||
final viewOnly = ffi.ffiModel.viewOnly;
|
|
||||||
final visible = !viewOnly && ffi.ffiModel.keyboard;
|
|
||||||
if (!visible) return Offstage();
|
|
||||||
return _MenuItemButton(
|
|
||||||
child: Text(translate('Insert Lock')),
|
|
||||||
ffi: ffi,
|
|
||||||
onPressed: () => bind.sessionLockScreen(id: id));
|
|
||||||
}
|
|
||||||
|
|
||||||
blockUserInput() {
|
|
||||||
final pi = ffi.ffiModel.pi;
|
|
||||||
final visible =
|
|
||||||
ffi.ffiModel.keyboard && pi.platform == kPeerPlatformWindows;
|
|
||||||
if (!visible) return Offstage();
|
|
||||||
return _MenuItemButton(
|
|
||||||
child: Obx(() => Text(translate(
|
|
||||||
'${BlockInputState.find(id).value ? 'Unb' : 'B'}lock user input'))),
|
|
||||||
ffi: ffi,
|
|
||||||
onPressed: () {
|
|
||||||
RxBool blockInput = BlockInputState.find(id);
|
|
||||||
bind.sessionToggleOption(
|
|
||||||
id: id, value: '${blockInput.value ? 'un' : ''}block-input');
|
|
||||||
blockInput.value = !blockInput.value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
switchSides() {
|
|
||||||
final pi = ffi.ffiModel.pi;
|
|
||||||
final visible = ffi.ffiModel.keyboard &&
|
|
||||||
pi.platform != kPeerPlatformAndroid &&
|
|
||||||
pi.platform != kPeerPlatformMacOS &&
|
|
||||||
version_cmp(pi.version, '1.2.0') >= 0;
|
|
||||||
if (!visible) return Offstage();
|
|
||||||
return _MenuItemButton(
|
|
||||||
child: Text(translate('Switch Sides')),
|
|
||||||
ffi: ffi,
|
|
||||||
onPressed: () => _showConfirmSwitchSidesDialog(id, ffi.dialogManager));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showConfirmSwitchSidesDialog(
|
|
||||||
String id, OverlayDialogManager dialogManager) async {
|
|
||||||
dialogManager.show((setState, close) {
|
|
||||||
submit() async {
|
|
||||||
await bind.sessionSwitchSides(id: id);
|
|
||||||
closeConnection(id: id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return CustomAlertDialog(
|
|
||||||
content: msgboxContent('info', 'Switch Sides',
|
|
||||||
'Please confirm if you want to share your desktop?'),
|
|
||||||
actions: [
|
|
||||||
dialogButton('Cancel', onPressed: close, isOutline: true),
|
|
||||||
dialogButton('OK', onPressed: submit),
|
|
||||||
],
|
|
||||||
onSubmit: submit,
|
|
||||||
onCancel: close,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh() {
|
|
||||||
final pi = ffi.ffiModel.pi;
|
|
||||||
final visible = pi.version.isNotEmpty;
|
|
||||||
if (!visible) return Offstage();
|
|
||||||
return _MenuItemButton(
|
|
||||||
child: Text(translate('Refresh')),
|
|
||||||
ffi: ffi,
|
|
||||||
onPressed: () => bind.sessionRefresh(id: id));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -891,6 +679,8 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
|
|
||||||
PeerInfo get pi => widget.ffi.ffiModel.pi;
|
PeerInfo get pi => widget.ffi.ffiModel.pi;
|
||||||
FfiModel get ffiModel => widget.ffi.ffiModel;
|
FfiModel get ffiModel => widget.ffi.ffiModel;
|
||||||
|
FFI get ffi => widget.ffi;
|
||||||
|
String get id => widget.id;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -909,30 +699,26 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
codec(),
|
codec(),
|
||||||
resolutions(),
|
resolutions(),
|
||||||
Divider(),
|
Divider(),
|
||||||
showRemoteCursor(),
|
toggles(),
|
||||||
zoomCursor(),
|
|
||||||
showQualityMonitor(),
|
|
||||||
mute(),
|
|
||||||
fileCopyAndPaste(),
|
|
||||||
disableClipboard(),
|
|
||||||
lockAfterSessionEnd(),
|
|
||||||
privacyMode(),
|
|
||||||
swapKey(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
adjustWindow() {
|
adjustWindow() {
|
||||||
final visible = _isWindowCanBeAdjusted();
|
return futureBuilder(
|
||||||
if (!visible) return Offstage();
|
future: _isWindowCanBeAdjusted(),
|
||||||
return Column(
|
hasData: (data) {
|
||||||
children: [
|
final visible = data as bool;
|
||||||
_MenuItemButton(
|
if (!visible) return Offstage();
|
||||||
child: Text(translate('Adjust Window')),
|
return Column(
|
||||||
onPressed: _doAdjustWindow,
|
children: [
|
||||||
ffi: widget.ffi),
|
_MenuItemButton(
|
||||||
Divider(),
|
child: Text(translate('Adjust Window')),
|
||||||
],
|
onPressed: _doAdjustWindow,
|
||||||
);
|
ffi: widget.ffi),
|
||||||
|
Divider(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_doAdjustWindow() async {
|
_doAdjustWindow() async {
|
||||||
@ -1004,8 +790,9 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_isWindowCanBeAdjusted() {
|
Future<bool> _isWindowCanBeAdjusted() async {
|
||||||
if (widget.state.viewStyle.value != kRemoteViewStyleOriginal) {
|
final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? '';
|
||||||
|
if (viewStyle != kRemoteViewStyleOriginal) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final remoteCount = RemoteCountState.find().value;
|
final remoteCount = RemoteCountState.find().value;
|
||||||
@ -1035,47 +822,34 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewStyle() {
|
viewStyle() {
|
||||||
return futureBuilder(future: () async {
|
return futureBuilder(
|
||||||
final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? '';
|
future: toolbarViewStyle(context, widget.id, widget.ffi),
|
||||||
widget.state.viewStyle.value = viewStyle;
|
hasData: (data) {
|
||||||
return viewStyle;
|
final v = data as List<TRadioMenu<String>>;
|
||||||
}(), hasData: (data) {
|
return Column(children: [
|
||||||
final groupValue = data as String;
|
...v
|
||||||
onChanged(String? value) async {
|
.map((e) => _RadioMenuButton<String>(
|
||||||
if (value == null) return;
|
value: e.value,
|
||||||
await bind.sessionSetViewStyle(id: widget.id, value: value);
|
groupValue: e.groupValue,
|
||||||
widget.state.viewStyle.value = value;
|
onChanged: e.onChanged,
|
||||||
widget.ffi.canvasModel.updateViewStyle();
|
child: e.child,
|
||||||
}
|
ffi: ffi))
|
||||||
|
.toList(),
|
||||||
return Column(children: [
|
Divider(),
|
||||||
_RadioMenuButton<String>(
|
]);
|
||||||
child: Text(translate('Scale original')),
|
});
|
||||||
value: kRemoteViewStyleOriginal,
|
|
||||||
groupValue: groupValue,
|
|
||||||
onChanged: onChanged,
|
|
||||||
ffi: widget.ffi,
|
|
||||||
),
|
|
||||||
_RadioMenuButton<String>(
|
|
||||||
child: Text(translate('Scale adaptive')),
|
|
||||||
value: kRemoteViewStyleAdaptive,
|
|
||||||
groupValue: groupValue,
|
|
||||||
onChanged: onChanged,
|
|
||||||
ffi: widget.ffi,
|
|
||||||
),
|
|
||||||
Divider(),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollStyle() {
|
scrollStyle() {
|
||||||
final visible = widget.state.viewStyle.value == kRemoteViewStyleOriginal;
|
|
||||||
if (!visible) return Offstage();
|
|
||||||
return futureBuilder(future: () async {
|
return futureBuilder(future: () async {
|
||||||
|
final viewStyle = await bind.sessionGetViewStyle(id: id) ?? '';
|
||||||
|
final visible = viewStyle == kRemoteViewStyleOriginal;
|
||||||
final scrollStyle = await bind.sessionGetScrollStyle(id: widget.id) ?? '';
|
final scrollStyle = await bind.sessionGetScrollStyle(id: widget.id) ?? '';
|
||||||
return scrollStyle;
|
return {'visible': visible, 'scrollStyle': scrollStyle};
|
||||||
}(), hasData: (data) {
|
}(), hasData: (data) {
|
||||||
final groupValue = data as String;
|
final visible = data['visible'] as bool;
|
||||||
|
if (!visible) return Offstage();
|
||||||
|
final groupValue = data['scrollStyle'] as String;
|
||||||
onChange(String? value) async {
|
onChange(String? value) async {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await bind.sessionSetScrollStyle(id: widget.id, value: value);
|
await bind.sessionSetScrollStyle(id: widget.id, value: value);
|
||||||
@ -1104,269 +878,44 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
imageQuality() {
|
imageQuality() {
|
||||||
return futureBuilder(future: () async {
|
return futureBuilder(
|
||||||
final imageQuality =
|
future: toolbarImageQuality(context, widget.id, widget.ffi),
|
||||||
await bind.sessionGetImageQuality(id: widget.id) ?? '';
|
hasData: (data) {
|
||||||
return imageQuality;
|
final v = data as List<TRadioMenu<String>>;
|
||||||
}(), hasData: (data) {
|
return _SubmenuButton(
|
||||||
final groupValue = data as String;
|
|
||||||
onChanged(String? value) async {
|
|
||||||
if (value == null) return;
|
|
||||||
await bind.sessionSetImageQuality(id: widget.id, value: value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _SubmenuButton(
|
|
||||||
ffi: widget.ffi,
|
|
||||||
child: Text(translate('Image Quality')),
|
|
||||||
menuChildren: [
|
|
||||||
_RadioMenuButton<String>(
|
|
||||||
child: Text(translate('Good image quality')),
|
|
||||||
value: kRemoteImageQualityBest,
|
|
||||||
groupValue: groupValue,
|
|
||||||
onChanged: onChanged,
|
|
||||||
ffi: widget.ffi,
|
ffi: widget.ffi,
|
||||||
),
|
child: Text(translate('Image Quality')),
|
||||||
_RadioMenuButton<String>(
|
menuChildren: v
|
||||||
child: Text(translate('Balanced')),
|
.map((e) => _RadioMenuButton<String>(
|
||||||
value: kRemoteImageQualityBalanced,
|
value: e.value,
|
||||||
groupValue: groupValue,
|
groupValue: e.groupValue,
|
||||||
onChanged: onChanged,
|
onChanged: e.onChanged,
|
||||||
ffi: widget.ffi,
|
child: e.child,
|
||||||
),
|
ffi: ffi))
|
||||||
_RadioMenuButton<String>(
|
.toList(),
|
||||||
child: Text(translate('Optimize reaction time')),
|
);
|
||||||
value: kRemoteImageQualityLow,
|
});
|
||||||
groupValue: groupValue,
|
|
||||||
onChanged: onChanged,
|
|
||||||
ffi: widget.ffi,
|
|
||||||
),
|
|
||||||
_RadioMenuButton<String>(
|
|
||||||
child: Text(translate('Custom')),
|
|
||||||
value: kRemoteImageQualityCustom,
|
|
||||||
groupValue: groupValue,
|
|
||||||
onChanged: (value) {
|
|
||||||
onChanged(value);
|
|
||||||
_customImageQualityDialog();
|
|
||||||
},
|
|
||||||
ffi: widget.ffi,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_customImageQualityDialog() async {
|
|
||||||
double qualityInitValue = 50;
|
|
||||||
double fpsInitValue = 30;
|
|
||||||
bool qualitySet = false;
|
|
||||||
bool fpsSet = false;
|
|
||||||
setCustomValues({double? quality, double? fps}) async {
|
|
||||||
if (quality != null) {
|
|
||||||
qualitySet = true;
|
|
||||||
await bind.sessionSetCustomImageQuality(
|
|
||||||
id: widget.id, value: quality.toInt());
|
|
||||||
}
|
|
||||||
if (fps != null) {
|
|
||||||
fpsSet = true;
|
|
||||||
await bind.sessionSetCustomFps(id: widget.id, fps: fps.toInt());
|
|
||||||
}
|
|
||||||
if (!qualitySet) {
|
|
||||||
qualitySet = true;
|
|
||||||
await bind.sessionSetCustomImageQuality(
|
|
||||||
id: widget.id, value: qualityInitValue.toInt());
|
|
||||||
}
|
|
||||||
if (!fpsSet) {
|
|
||||||
fpsSet = true;
|
|
||||||
await bind.sessionSetCustomFps(
|
|
||||||
id: widget.id, fps: fpsInitValue.toInt());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final btnClose = dialogButton('Close', onPressed: () async {
|
|
||||||
await setCustomValues();
|
|
||||||
widget.ffi.dialogManager.dismissAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
// quality
|
|
||||||
final quality = await bind.sessionGetCustomImageQuality(id: widget.id);
|
|
||||||
qualityInitValue =
|
|
||||||
quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0;
|
|
||||||
const qualityMinValue = 10.0;
|
|
||||||
const qualityMaxValue = 100.0;
|
|
||||||
if (qualityInitValue < qualityMinValue) {
|
|
||||||
qualityInitValue = qualityMinValue;
|
|
||||||
}
|
|
||||||
if (qualityInitValue > qualityMaxValue) {
|
|
||||||
qualityInitValue = qualityMaxValue;
|
|
||||||
}
|
|
||||||
final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
|
|
||||||
final debouncerQuality = Debouncer<double>(
|
|
||||||
Duration(milliseconds: 1000),
|
|
||||||
onChanged: (double v) {
|
|
||||||
setCustomValues(quality: v);
|
|
||||||
},
|
|
||||||
initialValue: qualityInitValue,
|
|
||||||
);
|
|
||||||
final qualitySlider = Obx(() => Row(
|
|
||||||
children: [
|
|
||||||
Slider(
|
|
||||||
value: qualitySliderValue.value,
|
|
||||||
min: qualityMinValue,
|
|
||||||
max: qualityMaxValue,
|
|
||||||
divisions: 18,
|
|
||||||
onChanged: (double value) {
|
|
||||||
qualitySliderValue.value = value;
|
|
||||||
debouncerQuality.value = value;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 40,
|
|
||||||
child: Text(
|
|
||||||
'${qualitySliderValue.value.round()}%',
|
|
||||||
style: const TextStyle(fontSize: 15),
|
|
||||||
)),
|
|
||||||
SizedBox(
|
|
||||||
width: 50,
|
|
||||||
child: Text(
|
|
||||||
translate('Bitrate'),
|
|
||||||
style: const TextStyle(fontSize: 15),
|
|
||||||
))
|
|
||||||
],
|
|
||||||
));
|
|
||||||
// fps
|
|
||||||
final fpsOption =
|
|
||||||
await bind.sessionGetOption(id: widget.id, arg: 'custom-fps');
|
|
||||||
fpsInitValue = fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30;
|
|
||||||
if (fpsInitValue < 5 || fpsInitValue > 120) {
|
|
||||||
fpsInitValue = 30;
|
|
||||||
}
|
|
||||||
final RxDouble fpsSliderValue = RxDouble(fpsInitValue);
|
|
||||||
final debouncerFps = Debouncer<double>(
|
|
||||||
Duration(milliseconds: 1000),
|
|
||||||
onChanged: (double v) {
|
|
||||||
setCustomValues(fps: v);
|
|
||||||
},
|
|
||||||
initialValue: qualityInitValue,
|
|
||||||
);
|
|
||||||
bool? direct;
|
|
||||||
try {
|
|
||||||
direct = ConnectionTypeState.find(widget.id).direct.value ==
|
|
||||||
ConnectionType.strDirect;
|
|
||||||
} catch (_) {}
|
|
||||||
final fpsSlider = Offstage(
|
|
||||||
offstage: (await bind.mainIsUsingPublicServer() && direct != true) ||
|
|
||||||
version_cmp(pi.version, '1.2.0') < 0,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Obx((() => Slider(
|
|
||||||
value: fpsSliderValue.value,
|
|
||||||
min: 5,
|
|
||||||
max: 120,
|
|
||||||
divisions: 23,
|
|
||||||
onChanged: (double value) {
|
|
||||||
fpsSliderValue.value = value;
|
|
||||||
debouncerFps.value = value;
|
|
||||||
},
|
|
||||||
))),
|
|
||||||
SizedBox(
|
|
||||||
width: 40,
|
|
||||||
child: Obx(() => Text(
|
|
||||||
'${fpsSliderValue.value.round()}',
|
|
||||||
style: const TextStyle(fontSize: 15),
|
|
||||||
))),
|
|
||||||
SizedBox(
|
|
||||||
width: 50,
|
|
||||||
child: Text(
|
|
||||||
translate('FPS'),
|
|
||||||
style: const TextStyle(fontSize: 15),
|
|
||||||
))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final content = Column(
|
|
||||||
children: [qualitySlider, fpsSlider],
|
|
||||||
);
|
|
||||||
msgBoxCommon(
|
|
||||||
widget.ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
codec() {
|
codec() {
|
||||||
return futureBuilder(future: () async {
|
return futureBuilder(
|
||||||
final alternativeCodecs =
|
future: toolbarCodec(context, id, ffi),
|
||||||
await bind.sessionAlternativeCodecs(id: widget.id);
|
hasData: (data) {
|
||||||
final codecPreference =
|
final v = data as List<TRadioMenu<String>>;
|
||||||
await bind.sessionGetOption(id: widget.id, arg: 'codec-preference') ??
|
if (v.isEmpty) return Offstage();
|
||||||
'';
|
|
||||||
return {
|
|
||||||
'alternativeCodecs': alternativeCodecs,
|
|
||||||
'codecPreference': codecPreference
|
|
||||||
};
|
|
||||||
}(), hasData: (data) {
|
|
||||||
final List<bool> codecs = [];
|
|
||||||
try {
|
|
||||||
final Map codecsJson = jsonDecode(data['alternativeCodecs']);
|
|
||||||
final vp8 = codecsJson['vp8'] ?? false;
|
|
||||||
final h264 = codecsJson['h264'] ?? false;
|
|
||||||
final h265 = codecsJson['h265'] ?? false;
|
|
||||||
codecs.add(vp8);
|
|
||||||
codecs.add(h264);
|
|
||||||
codecs.add(h265);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Show Codec Preference err=$e");
|
|
||||||
}
|
|
||||||
final visible =
|
|
||||||
codecs.length == 3 && (codecs[0] || codecs[1] || codecs[2]);
|
|
||||||
if (!visible) return Offstage();
|
|
||||||
final groupValue = data['codecPreference'] as String;
|
|
||||||
onChanged(String? value) async {
|
|
||||||
if (value == null) return;
|
|
||||||
await bind.sessionPeerOption(
|
|
||||||
id: widget.id, name: 'codec-preference', value: value);
|
|
||||||
bind.sessionChangePreferCodec(id: widget.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _SubmenuButton(
|
return _SubmenuButton(
|
||||||
ffi: widget.ffi,
|
|
||||||
child: Text(translate('Codec')),
|
|
||||||
menuChildren: [
|
|
||||||
_RadioMenuButton<String>(
|
|
||||||
child: Text(translate('Auto')),
|
|
||||||
value: 'auto',
|
|
||||||
groupValue: groupValue,
|
|
||||||
onChanged: onChanged,
|
|
||||||
ffi: widget.ffi,
|
ffi: widget.ffi,
|
||||||
),
|
child: Text(translate('Codec')),
|
||||||
_RadioMenuButton<String>(
|
menuChildren: v
|
||||||
child: Text(translate('VP8')),
|
.map((e) => _RadioMenuButton(
|
||||||
value: 'vp8',
|
value: e.value,
|
||||||
groupValue: groupValue,
|
groupValue: e.groupValue,
|
||||||
onChanged: codecs[0] ? onChanged : null,
|
onChanged: e.onChanged,
|
||||||
ffi: widget.ffi,
|
child: e.child,
|
||||||
),
|
ffi: ffi))
|
||||||
_RadioMenuButton<String>(
|
.toList());
|
||||||
child: Text(translate('VP9')),
|
});
|
||||||
value: 'vp9',
|
|
||||||
groupValue: groupValue,
|
|
||||||
onChanged: onChanged,
|
|
||||||
ffi: widget.ffi,
|
|
||||||
),
|
|
||||||
_RadioMenuButton<String>(
|
|
||||||
child: Text(translate('H264')),
|
|
||||||
value: 'h264',
|
|
||||||
groupValue: groupValue,
|
|
||||||
onChanged: codecs[1] ? onChanged : null,
|
|
||||||
ffi: widget.ffi,
|
|
||||||
),
|
|
||||||
_RadioMenuButton<String>(
|
|
||||||
child: Text(translate('H265')),
|
|
||||||
value: 'h265',
|
|
||||||
groupValue: groupValue,
|
|
||||||
onChanged: codecs[2] ? onChanged : null,
|
|
||||||
ffi: widget.ffi,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resolutions() {
|
resolutions() {
|
||||||
@ -1387,7 +936,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
Future.delayed(Duration(seconds: 3), () async {
|
Future.delayed(Duration(seconds: 3), () async {
|
||||||
final display = ffiModel.display;
|
final display = ffiModel.display;
|
||||||
if (w == display.width && h == display.height) {
|
if (w == display.width && h == display.height) {
|
||||||
if (_isWindowCanBeAdjusted()) {
|
if (await _isWindowCanBeAdjusted()) {
|
||||||
_doAdjustWindow();
|
_doAdjustWindow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1409,169 +958,21 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
child: Text(translate("Resolution")));
|
child: Text(translate("Resolution")));
|
||||||
}
|
}
|
||||||
|
|
||||||
showRemoteCursor() {
|
toggles() {
|
||||||
if (pi.platform == kPeerPlatformAndroid) {
|
return futureBuilder(
|
||||||
return Offstage();
|
future: toolbarDisplayToggle(context, id, ffi),
|
||||||
}
|
hasData: (data) {
|
||||||
final visible =
|
final v = data as List<TToggleMenu>;
|
||||||
!widget.ffi.canvasModel.cursorEmbedded && !ffiModel.pi.is_wayland;
|
if (v.isEmpty) return Offstage();
|
||||||
if (!visible) return Offstage();
|
return Column(
|
||||||
final enabled = !ffiModel.viewOnly;
|
children: v
|
||||||
final state = ShowRemoteCursorState.find(widget.id);
|
.map((e) => _CheckboxMenuButton(
|
||||||
final option = 'show-remote-cursor';
|
value: e.value,
|
||||||
return _CheckboxMenuButton(
|
onChanged: e.onChanged,
|
||||||
value: state.value,
|
child: e.child,
|
||||||
onChanged: enabled
|
ffi: ffi))
|
||||||
? (value) async {
|
.toList());
|
||||||
if (value == null) return;
|
});
|
||||||
await bind.sessionToggleOption(id: widget.id, value: option);
|
|
||||||
state.value =
|
|
||||||
bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
ffi: widget.ffi,
|
|
||||||
child: Text(translate('Show remote cursor')));
|
|
||||||
}
|
|
||||||
|
|
||||||
zoomCursor() {
|
|
||||||
if (pi.platform == kPeerPlatformAndroid) {
|
|
||||||
return Offstage();
|
|
||||||
}
|
|
||||||
final visible = widget.state.viewStyle.value != kRemoteViewStyleOriginal;
|
|
||||||
if (!visible) return Offstage();
|
|
||||||
final option = 'zoom-cursor';
|
|
||||||
final peerState = PeerBoolOption.find(widget.id, option);
|
|
||||||
return _CheckboxMenuButton(
|
|
||||||
value: peerState.value,
|
|
||||||
onChanged: (value) async {
|
|
||||||
if (value == null) return;
|
|
||||||
await bind.sessionToggleOption(id: widget.id, value: option);
|
|
||||||
peerState.value =
|
|
||||||
bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
|
|
||||||
},
|
|
||||||
ffi: widget.ffi,
|
|
||||||
child: Text(translate('Zoom cursor')));
|
|
||||||
}
|
|
||||||
|
|
||||||
showQualityMonitor() {
|
|
||||||
final option = 'show-quality-monitor';
|
|
||||||
final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
|
|
||||||
return _CheckboxMenuButton(
|
|
||||||
value: value,
|
|
||||||
onChanged: (value) async {
|
|
||||||
if (value == null) return;
|
|
||||||
await bind.sessionToggleOption(id: widget.id, value: option);
|
|
||||||
widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
|
|
||||||
},
|
|
||||||
ffi: widget.ffi,
|
|
||||||
child: Text(translate('Show quality monitor')));
|
|
||||||
}
|
|
||||||
|
|
||||||
mute() {
|
|
||||||
final visible = perms['audio'] != false;
|
|
||||||
if (!visible) return Offstage();
|
|
||||||
final option = 'disable-audio';
|
|
||||||
final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
|
|
||||||
return _CheckboxMenuButton(
|
|
||||||
value: value,
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value == null) return;
|
|
||||||
bind.sessionToggleOption(id: widget.id, value: option);
|
|
||||||
},
|
|
||||||
ffi: widget.ffi,
|
|
||||||
child: Text(translate('Mute')));
|
|
||||||
}
|
|
||||||
|
|
||||||
fileCopyAndPaste() {
|
|
||||||
final visible = Platform.isWindows &&
|
|
||||||
pi.platform == kPeerPlatformWindows &&
|
|
||||||
perms['file'] != false;
|
|
||||||
if (!visible) return Offstage();
|
|
||||||
final option = 'enable-file-transfer';
|
|
||||||
final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
|
|
||||||
return _CheckboxMenuButton(
|
|
||||||
value: value,
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value == null) return;
|
|
||||||
bind.sessionToggleOption(id: widget.id, value: option);
|
|
||||||
},
|
|
||||||
ffi: widget.ffi,
|
|
||||||
child: Text(translate('Allow file copy and paste')));
|
|
||||||
}
|
|
||||||
|
|
||||||
disableClipboard() {
|
|
||||||
final visible = ffiModel.keyboard && perms['clipboard'] != false;
|
|
||||||
if (!visible) return Offstage();
|
|
||||||
final enabled = !ffiModel.viewOnly;
|
|
||||||
final option = 'disable-clipboard';
|
|
||||||
var value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
|
|
||||||
if (ffiModel.viewOnly) value = true;
|
|
||||||
return _CheckboxMenuButton(
|
|
||||||
value: value,
|
|
||||||
onChanged: enabled
|
|
||||||
? (value) {
|
|
||||||
if (value == null) return;
|
|
||||||
bind.sessionToggleOption(id: widget.id, value: option);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
ffi: widget.ffi,
|
|
||||||
child: Text(translate('Disable clipboard')));
|
|
||||||
}
|
|
||||||
|
|
||||||
lockAfterSessionEnd() {
|
|
||||||
if (!ffiModel.keyboard) return Offstage();
|
|
||||||
final option = 'lock-after-session-end';
|
|
||||||
final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
|
|
||||||
return _CheckboxMenuButton(
|
|
||||||
value: value,
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value == null) return;
|
|
||||||
bind.sessionToggleOption(id: widget.id, value: option);
|
|
||||||
},
|
|
||||||
ffi: widget.ffi,
|
|
||||||
child: Text(translate('Lock after session end')));
|
|
||||||
}
|
|
||||||
|
|
||||||
privacyMode() {
|
|
||||||
bool visible = ffiModel.keyboard && pi.features.privacyMode;
|
|
||||||
if (!visible) return Offstage();
|
|
||||||
final option = 'privacy-mode';
|
|
||||||
final rxValue = PrivacyModeState.find(widget.id);
|
|
||||||
return _CheckboxMenuButton(
|
|
||||||
value: rxValue.value,
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value == null) return;
|
|
||||||
if (ffiModel.pi.currentDisplay != 0) {
|
|
||||||
msgBox(
|
|
||||||
widget.id,
|
|
||||||
'custom-nook-nocancel-hasclose',
|
|
||||||
'info',
|
|
||||||
'Please switch to Display 1 first',
|
|
||||||
'',
|
|
||||||
widget.ffi.dialogManager);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
bind.sessionToggleOption(id: widget.id, value: option);
|
|
||||||
},
|
|
||||||
ffi: widget.ffi,
|
|
||||||
child: Text(translate('Privacy mode')));
|
|
||||||
}
|
|
||||||
|
|
||||||
swapKey() {
|
|
||||||
final visible = ffiModel.keyboard &&
|
|
||||||
((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) ||
|
|
||||||
(!Platform.isMacOS && pi.platform == kPeerPlatformMacOS));
|
|
||||||
if (!visible) return Offstage();
|
|
||||||
final option = 'allow_swap_key';
|
|
||||||
final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
|
|
||||||
return _CheckboxMenuButton(
|
|
||||||
value: value,
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value == null) return;
|
|
||||||
bind.sessionToggleOption(id: widget.id, value: option);
|
|
||||||
},
|
|
||||||
ffi: widget.ffi,
|
|
||||||
child: Text(translate('Swap control-command key')));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1799,19 +1200,22 @@ class _RecordMenu extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var ffi = Provider.of<FfiModel>(context);
|
var ffi = Provider.of<FfiModel>(context);
|
||||||
final visible = ffi.permissions['recording'] != false;
|
var recordingModel = Provider.of<RecordingModel>(context);
|
||||||
|
final visible =
|
||||||
|
recordingModel.start || ffi.permissions['recording'] != false;
|
||||||
if (!visible) return Offstage();
|
if (!visible) return Offstage();
|
||||||
return Consumer<RecordingModel>(
|
return _IconMenuButton(
|
||||||
builder: (context, value, child) => _IconMenuButton(
|
assetName: 'assets/rec.svg',
|
||||||
assetName: 'assets/rec.svg',
|
tooltip: recordingModel.start
|
||||||
tooltip:
|
? 'Stop session recording'
|
||||||
value.start ? 'Stop session recording' : 'Start session recording',
|
: 'Start session recording',
|
||||||
onPressed: () => value.toggle(),
|
onPressed: () => recordingModel.toggle(),
|
||||||
color: value.start ? _MenubarTheme.redColor : _MenubarTheme.blueColor,
|
color: recordingModel.start
|
||||||
hoverColor: value.start
|
? _MenubarTheme.redColor
|
||||||
? _MenubarTheme.hoverRedColor
|
: _MenubarTheme.blueColor,
|
||||||
: _MenubarTheme.hoverBlueColor,
|
hoverColor: recordingModel.start
|
||||||
),
|
? _MenubarTheme.hoverRedColor
|
||||||
|
: _MenubarTheme.hoverBlueColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,13 @@ import 'dart:ui' as ui;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/common/shared_state.dart';
|
||||||
|
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
|
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
|
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:wakelock/wakelock.dart';
|
import 'package:wakelock/wakelock.dart';
|
||||||
@ -69,6 +72,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
keyboardSubscription =
|
keyboardSubscription =
|
||||||
keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
|
keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
|
||||||
_blockableOverlayState.applyFfi(gFFI);
|
_blockableOverlayState.applyFfi(gFFI);
|
||||||
|
initSharedStates(widget.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -85,6 +89,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
overlays: SystemUiOverlay.values);
|
overlays: SystemUiOverlay.values);
|
||||||
Wakelock.disable();
|
Wakelock.disable();
|
||||||
keyboardSubscription.cancel();
|
keyboardSubscription.cancel();
|
||||||
|
removeSharedStates(widget.id);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -543,150 +548,21 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
final x = 120.0;
|
final x = 120.0;
|
||||||
final y = size.height;
|
final y = size.height;
|
||||||
final more = <PopupMenuItem<String>>[];
|
final menus = toolbarControls(context, id, gFFI);
|
||||||
final pi = gFFI.ffiModel.pi;
|
final more = menus
|
||||||
final perms = gFFI.ffiModel.permissions;
|
.asMap()
|
||||||
if (pi.version.isNotEmpty) {
|
.entries
|
||||||
more.add(PopupMenuItem<String>(
|
.map((e) => PopupMenuItem<int>(child: e.value.child, value: e.key))
|
||||||
child: Text(translate('Refresh')), value: 'refresh'));
|
.toList();
|
||||||
}
|
|
||||||
if (gFFI.ffiModel.pi.is_headless) {
|
|
||||||
more.add(
|
|
||||||
PopupMenuItem<String>(
|
|
||||||
child: Row(
|
|
||||||
children: ([
|
|
||||||
Text(translate('OS Account')),
|
|
||||||
TextButton(
|
|
||||||
style: flatButtonStyle,
|
|
||||||
onPressed: () {
|
|
||||||
showSetOSAccount(id, gFFI.dialogManager);
|
|
||||||
},
|
|
||||||
child: Icon(Icons.edit, color: MyTheme.accent),
|
|
||||||
)
|
|
||||||
])),
|
|
||||||
value: 'enter_os_account'),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
more.add(
|
|
||||||
PopupMenuItem<String>(
|
|
||||||
child: Row(
|
|
||||||
children: ([
|
|
||||||
Text(translate('OS Password')),
|
|
||||||
TextButton(
|
|
||||||
style: flatButtonStyle,
|
|
||||||
onPressed: () {
|
|
||||||
showSetOSPassword(id, false, gFFI.dialogManager);
|
|
||||||
},
|
|
||||||
child: Icon(Icons.edit, color: MyTheme.accent),
|
|
||||||
)
|
|
||||||
])),
|
|
||||||
value: 'enter_os_password'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!isWebDesktop) {
|
|
||||||
if (perms['keyboard'] != false && perms['clipboard'] != false) {
|
|
||||||
more.add(PopupMenuItem<String>(
|
|
||||||
child: Text(translate('Paste')), value: 'paste'));
|
|
||||||
}
|
|
||||||
more.add(PopupMenuItem<String>(
|
|
||||||
child: Text(translate('Reset canvas')), value: 'reset_canvas'));
|
|
||||||
}
|
|
||||||
if (perms['keyboard'] != false) {
|
|
||||||
// * Currently mobile does not enable map mode
|
|
||||||
// more.add(PopupMenuItem<String>(
|
|
||||||
// child: Text(translate('Physical Keyboard Input Mode')),
|
|
||||||
// value: 'input-mode'));
|
|
||||||
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
|
|
||||||
more.add(PopupMenuItem<String>(
|
|
||||||
child: Text('${translate('Insert')} Ctrl + Alt + Del'),
|
|
||||||
value: 'cad'));
|
|
||||||
}
|
|
||||||
more.add(PopupMenuItem<String>(
|
|
||||||
child: Text(translate('Insert Lock')), value: 'lock'));
|
|
||||||
if (pi.platform == kPeerPlatformWindows &&
|
|
||||||
await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') !=
|
|
||||||
true) {
|
|
||||||
more.add(PopupMenuItem<String>(
|
|
||||||
child: Text(translate(
|
|
||||||
'${gFFI.ffiModel.inputBlocked ? 'Unb' : 'B'}lock user input')),
|
|
||||||
value: 'block-input'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (perms["restart"] != false &&
|
|
||||||
(pi.platform == kPeerPlatformLinux ||
|
|
||||||
pi.platform == kPeerPlatformWindows ||
|
|
||||||
pi.platform == kPeerPlatformMacOS)) {
|
|
||||||
more.add(PopupMenuItem<String>(
|
|
||||||
child: Text(translate('Restart Remote Device')), value: 'restart'));
|
|
||||||
}
|
|
||||||
// Currently only support VP9
|
|
||||||
if (gFFI.recordingModel.start ||
|
|
||||||
(perms["recording"] != false &&
|
|
||||||
gFFI.qualityMonitorModel.data.codecFormat == "VP9")) {
|
|
||||||
more.add(PopupMenuItem<String>(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(translate(gFFI.recordingModel.start
|
|
||||||
? 'Stop session recording'
|
|
||||||
: 'Start session recording')),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(left: 12),
|
|
||||||
child: Icon(
|
|
||||||
gFFI.recordingModel.start
|
|
||||||
? Icons.pause_circle_filled
|
|
||||||
: Icons.videocam_outlined,
|
|
||||||
color: MyTheme.accent),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
value: 'record'));
|
|
||||||
}
|
|
||||||
() async {
|
() async {
|
||||||
var value = await showMenu(
|
var index = await showMenu(
|
||||||
context: context,
|
context: context,
|
||||||
position: RelativeRect.fromLTRB(x, y, x, y),
|
position: RelativeRect.fromLTRB(x, y, x, y),
|
||||||
items: more,
|
items: more,
|
||||||
elevation: 8,
|
elevation: 8,
|
||||||
);
|
);
|
||||||
if (value == 'cad') {
|
if (index != null && index < menus.length) {
|
||||||
bind.sessionCtrlAltDel(id: widget.id);
|
menus[index].onPressed.call();
|
||||||
// * Currently mobile does not enable map mode
|
|
||||||
// } else if (value == 'input-mode') {
|
|
||||||
// changePhysicalKeyboardInputMode();
|
|
||||||
} else if (value == 'lock') {
|
|
||||||
bind.sessionLockScreen(id: widget.id);
|
|
||||||
} else if (value == 'block-input') {
|
|
||||||
bind.sessionToggleOption(
|
|
||||||
id: widget.id,
|
|
||||||
value: '${gFFI.ffiModel.inputBlocked ? 'un' : ''}block-input');
|
|
||||||
gFFI.ffiModel.inputBlocked = !gFFI.ffiModel.inputBlocked;
|
|
||||||
} else if (value == 'refresh') {
|
|
||||||
bind.sessionRefresh(id: widget.id);
|
|
||||||
} else if (value == 'paste') {
|
|
||||||
() async {
|
|
||||||
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
|
||||||
if (data != null && data.text != null) {
|
|
||||||
bind.sessionInputString(id: widget.id, value: data.text ?? "");
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
} else if (value == 'enter_os_password') {
|
|
||||||
// FIXME:
|
|
||||||
// null means no session of id
|
|
||||||
// empty string means no password
|
|
||||||
var password = await bind.sessionGetOption(id: id, arg: 'os-password');
|
|
||||||
if (password != null) {
|
|
||||||
bind.sessionInputOsPassword(id: widget.id, value: password);
|
|
||||||
} else {
|
|
||||||
showSetOSPassword(id, true, gFFI.dialogManager);
|
|
||||||
}
|
|
||||||
} else if (value == 'enter_os_account') {
|
|
||||||
showSetOSAccount(id, gFFI.dialogManager);
|
|
||||||
} else if (value == 'reset_canvas') {
|
|
||||||
gFFI.cursorModel.reset();
|
|
||||||
} else if (value == 'restart') {
|
|
||||||
showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager);
|
|
||||||
} else if (value == 'record') {
|
|
||||||
gFFI.recordingModel.toggle();
|
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
}
|
}
|
||||||
@ -941,14 +817,6 @@ class CursorPaint extends StatelessWidget {
|
|||||||
|
|
||||||
void showOptions(
|
void showOptions(
|
||||||
BuildContext context, String id, OverlayDialogManager dialogManager) async {
|
BuildContext context, String id, OverlayDialogManager dialogManager) async {
|
||||||
String quality =
|
|
||||||
await bind.sessionGetImageQuality(id: id) ?? kRemoteImageQualityBalanced;
|
|
||||||
if (quality == '') quality = kRemoteImageQualityBalanced;
|
|
||||||
String codec =
|
|
||||||
await bind.sessionGetOption(id: id, arg: 'codec-preference') ?? 'auto';
|
|
||||||
if (codec == '') codec = 'auto';
|
|
||||||
String viewStyle = await bind.sessionGetViewStyle(id: id) ?? '';
|
|
||||||
|
|
||||||
var displays = <Widget>[];
|
var displays = <Widget>[];
|
||||||
final pi = gFFI.ffiModel.pi;
|
final pi = gFFI.ffiModel.pi;
|
||||||
final image = gFFI.ffiModel.getConnectionImage();
|
final image = gFFI.ffiModel.getConnectionImage();
|
||||||
@ -991,107 +859,61 @@ void showOptions(
|
|||||||
if (displays.isNotEmpty) {
|
if (displays.isNotEmpty) {
|
||||||
displays.add(const Divider(color: MyTheme.border));
|
displays.add(const Divider(color: MyTheme.border));
|
||||||
}
|
}
|
||||||
final perms = gFFI.ffiModel.permissions;
|
|
||||||
final hasHwcodec = bind.mainHasHwcodec();
|
List<TRadioMenu<String>> viewStyleRadios =
|
||||||
final List<bool> codecs = [];
|
await toolbarViewStyle(context, id, gFFI);
|
||||||
try {
|
List<TRadioMenu<String>> imageQualityRadios =
|
||||||
final Map codecsJson =
|
await toolbarImageQuality(context, id, gFFI);
|
||||||
jsonDecode(await bind.sessionAlternativeCodecs(id: id));
|
List<TRadioMenu<String>> codecRadios = await toolbarCodec(context, id, gFFI);
|
||||||
final vp8 = codecsJson['vp8'] ?? false;
|
List<TToggleMenu> displayToggles =
|
||||||
final h264 = codecsJson['h264'] ?? false;
|
await toolbarDisplayToggle(context, id, gFFI);
|
||||||
final h265 = codecsJson['h265'] ?? false;
|
|
||||||
codecs.add(vp8);
|
|
||||||
codecs.add(h264);
|
|
||||||
codecs.add(h265);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Show Codec Preference err=$e");
|
|
||||||
}
|
|
||||||
|
|
||||||
dialogManager.show((setState, close) {
|
dialogManager.show((setState, close) {
|
||||||
final more = <Widget>[];
|
var viewStyle =
|
||||||
if (perms['audio'] != false) {
|
(viewStyleRadios.isNotEmpty ? viewStyleRadios[0].groupValue : '').obs;
|
||||||
more.add(getToggle(id, setState, 'disable-audio', 'Mute'));
|
var imageQuality =
|
||||||
}
|
(imageQualityRadios.isNotEmpty ? imageQualityRadios[0].groupValue : '')
|
||||||
if (perms['keyboard'] != false) {
|
.obs;
|
||||||
if (perms['clipboard'] != false) {
|
var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs;
|
||||||
more.add(
|
|
||||||
getToggle(id, setState, 'disable-clipboard', 'Disable clipboard'));
|
|
||||||
}
|
|
||||||
more.add(getToggle(
|
|
||||||
id, setState, 'lock-after-session-end', 'Lock after session end'));
|
|
||||||
if (pi.platform == kPeerPlatformWindows) {
|
|
||||||
more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setQuality(String? value) {
|
|
||||||
if (value == null) return;
|
|
||||||
setState(() {
|
|
||||||
quality = value;
|
|
||||||
bind.sessionSetImageQuality(id: id, value: value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setViewStyle(String? value) {
|
|
||||||
if (value == null) return;
|
|
||||||
setState(() {
|
|
||||||
viewStyle = value;
|
|
||||||
bind
|
|
||||||
.sessionSetViewStyle(id: id, value: value)
|
|
||||||
.then((_) => gFFI.canvasModel.updateViewStyle());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setCodec(String? value) {
|
|
||||||
if (value == null) return;
|
|
||||||
setState(() {
|
|
||||||
codec = value;
|
|
||||||
bind
|
|
||||||
.sessionPeerOption(id: id, name: "codec-preference", value: value)
|
|
||||||
.then((_) => bind.sessionChangePreferCodec(id: id));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final radios = [
|
final radios = [
|
||||||
getRadio(
|
for (var e in viewStyleRadios)
|
||||||
'Scale original', kRemoteViewStyleOriginal, viewStyle, setViewStyle),
|
Obx(() => getRadio<String>(e.child, e.value, viewStyle.value, (v) {
|
||||||
getRadio(
|
e.onChanged?.call(v);
|
||||||
'Scale adaptive', kRemoteViewStyleAdaptive, viewStyle, setViewStyle),
|
if (v != null) viewStyle.value = v;
|
||||||
|
})),
|
||||||
const Divider(color: MyTheme.border),
|
const Divider(color: MyTheme.border),
|
||||||
getRadio(
|
for (var e in imageQualityRadios)
|
||||||
'Good image quality', kRemoteImageQualityBest, quality, setQuality),
|
Obx(() => getRadio<String>(e.child, e.value, imageQuality.value, (v) {
|
||||||
getRadio('Balanced', kRemoteImageQualityBalanced, quality, setQuality),
|
e.onChanged?.call(v);
|
||||||
getRadio('Optimize reaction time', kRemoteImageQualityLow, quality,
|
if (v != null) imageQuality.value = v;
|
||||||
setQuality),
|
})),
|
||||||
const Divider(color: MyTheme.border)
|
const Divider(color: MyTheme.border),
|
||||||
|
for (var e in codecRadios)
|
||||||
|
Obx(() => getRadio<String>(e.child, e.value, codec.value, (v) {
|
||||||
|
e.onChanged?.call(v);
|
||||||
|
if (v != null) codec.value = v;
|
||||||
|
})),
|
||||||
|
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
|
||||||
];
|
];
|
||||||
|
final rxToggleValues = displayToggles.map((e) => e.value.obs).toList();
|
||||||
if (codecs.length == 3 && (codecs[0] || codecs[1] || codecs[2])) {
|
final toggles = displayToggles
|
||||||
radios.add(getRadio(translate('Auto'), 'auto', codec, setCodec));
|
.asMap()
|
||||||
if (codecs[0]) {
|
.entries
|
||||||
radios.add(getRadio('VP8', 'vp8', codec, setCodec));
|
.map((e) => Obx(() => CheckboxListTile(
|
||||||
}
|
contentPadding: EdgeInsets.zero,
|
||||||
radios.add(getRadio('VP9', 'vp9', codec, setCodec));
|
visualDensity: VisualDensity.compact,
|
||||||
if (codecs[1]) {
|
value: rxToggleValues[e.key].value,
|
||||||
radios.add(getRadio('H264', 'h264', codec, setCodec));
|
onChanged: (v) {
|
||||||
}
|
e.value.onChanged?.call(v);
|
||||||
if (codecs[2]) {
|
if (v != null) rxToggleValues[e.key].value = v;
|
||||||
radios.add(getRadio('H265', 'h265', codec, setCodec));
|
},
|
||||||
}
|
title: e.value.child)))
|
||||||
radios.add(const Divider(color: MyTheme.border));
|
.toList();
|
||||||
}
|
|
||||||
|
|
||||||
final toggles = [
|
|
||||||
getToggle(id, setState, 'show-quality-monitor', 'Show quality monitor'),
|
|
||||||
];
|
|
||||||
if (!gFFI.canvasModel.cursorEmbedded && !pi.is_wayland) {
|
|
||||||
toggles.insert(0,
|
|
||||||
getToggle(id, setState, 'show-remote-cursor', 'Show remote cursor'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: displays + radios + toggles + more),
|
children: displays + radios + toggles),
|
||||||
);
|
);
|
||||||
}, clickMaskDismiss: true, backDismiss: true);
|
}, clickMaskDismiss: true, backDismiss: true);
|
||||||
}
|
}
|
||||||
|
@ -504,13 +504,13 @@ void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
|||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
content: Column(
|
content: Column(
|
||||||
children: [
|
children: [
|
||||||
getRadio('Default', '', lang, setLang),
|
getRadio(Text(translate('Default')), '', lang, setLang),
|
||||||
Divider(color: MyTheme.border),
|
Divider(color: MyTheme.border),
|
||||||
] +
|
] +
|
||||||
langs.map((e) {
|
langs.map((e) {
|
||||||
final key = e[0] as String;
|
final key = e[0] as String;
|
||||||
final name = e[1] as String;
|
final name = e[1] as String;
|
||||||
return getRadio(name, key, lang, setLang);
|
return getRadio(Text(translate(name)), key, lang, setLang);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -536,9 +536,11 @@ void showThemeSettings(OverlayDialogManager dialogManager) async {
|
|||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
content: Column(children: [
|
content: Column(children: [
|
||||||
getRadio('Light', ThemeMode.light, themeMode, setTheme),
|
getRadio(
|
||||||
getRadio('Dark', ThemeMode.dark, themeMode, setTheme),
|
Text(translate('Light')), ThemeMode.light, themeMode, setTheme),
|
||||||
getRadio('Follow System', ThemeMode.system, themeMode, setTheme)
|
getRadio(Text(translate('Dark')), ThemeMode.dark, themeMode, setTheme),
|
||||||
|
getRadio(Text(translate('Follow System')), ThemeMode.system, themeMode,
|
||||||
|
setTheme)
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}, backDismiss: true, clickMaskDismiss: true);
|
}, backDismiss: true, clickMaskDismiss: true);
|
||||||
|
Loading…
Reference in New Issue
Block a user