toolbar.dart 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/services.dart';
  5. import 'package:flutter_hbb/common.dart';
  6. import 'package:flutter_hbb/common/shared_state.dart';
  7. import 'package:flutter_hbb/common/widgets/dialog.dart';
  8. import 'package:flutter_hbb/consts.dart';
  9. import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
  10. import 'package:flutter_hbb/models/model.dart';
  11. import 'package:flutter_hbb/models/platform_model.dart';
  12. import 'package:get/get.dart';
  13. bool isEditOsPassword = false;
  14. class TTextMenu {
  15. final Widget child;
  16. final VoidCallback? onPressed;
  17. Widget? trailingIcon;
  18. bool divider;
  19. TTextMenu(
  20. {required this.child,
  21. required this.onPressed,
  22. this.trailingIcon,
  23. this.divider = false});
  24. Widget getChild() {
  25. if (trailingIcon != null) {
  26. return Row(
  27. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  28. children: [
  29. child,
  30. trailingIcon!,
  31. ],
  32. );
  33. } else {
  34. return child;
  35. }
  36. }
  37. }
  38. class TRadioMenu<T> {
  39. final Widget child;
  40. final T value;
  41. final T groupValue;
  42. final ValueChanged<T?>? onChanged;
  43. TRadioMenu(
  44. {required this.child,
  45. required this.value,
  46. required this.groupValue,
  47. required this.onChanged});
  48. }
  49. class TToggleMenu {
  50. final Widget child;
  51. final bool value;
  52. final ValueChanged<bool?>? onChanged;
  53. TToggleMenu(
  54. {required this.child, required this.value, required this.onChanged});
  55. }
  56. handleOsPasswordEditIcon(
  57. SessionID sessionId, OverlayDialogManager dialogManager) {
  58. isEditOsPassword = true;
  59. showSetOSPassword(
  60. sessionId, false, dialogManager, null, () => isEditOsPassword = false);
  61. }
  62. handleOsPasswordAction(
  63. SessionID sessionId, OverlayDialogManager dialogManager) async {
  64. if (isEditOsPassword) {
  65. isEditOsPassword = false;
  66. return;
  67. }
  68. final password =
  69. await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ??
  70. '';
  71. if (password.isEmpty) {
  72. showSetOSPassword(sessionId, true, dialogManager, password,
  73. () => isEditOsPassword = false);
  74. } else {
  75. bind.sessionInputOsPassword(sessionId: sessionId, value: password);
  76. }
  77. }
  78. List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
  79. final ffiModel = ffi.ffiModel;
  80. final pi = ffiModel.pi;
  81. final perms = ffiModel.permissions;
  82. final sessionId = ffi.sessionId;
  83. final isDefaultConn = ffi.connType == ConnType.defaultConn;
  84. List<TTextMenu> v = [];
  85. // elevation
  86. if (isDefaultConn &&
  87. perms['keyboard'] != false &&
  88. ffi.elevationModel.showRequestMenu) {
  89. v.add(
  90. TTextMenu(
  91. child: Text(translate('Request Elevation')),
  92. onPressed: () =>
  93. showRequestElevationDialog(sessionId, ffi.dialogManager)),
  94. );
  95. }
  96. // osAccount / osPassword
  97. if (isDefaultConn && perms['keyboard'] != false) {
  98. v.add(
  99. TTextMenu(
  100. child: Row(children: [
  101. Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')),
  102. ]),
  103. trailingIcon: Transform.scale(
  104. scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
  105. child: IconButton(
  106. onPressed: () {
  107. if (isMobile && Navigator.canPop(context)) {
  108. Navigator.pop(context);
  109. }
  110. if (pi.isHeadless) {
  111. showSetOSAccount(sessionId, ffi.dialogManager);
  112. } else {
  113. handleOsPasswordEditIcon(sessionId, ffi.dialogManager);
  114. }
  115. },
  116. icon: Icon(Icons.edit, color: isMobile ? MyTheme.accent : null),
  117. ),
  118. ),
  119. onPressed: () => pi.isHeadless
  120. ? showSetOSAccount(sessionId, ffi.dialogManager)
  121. : handleOsPasswordAction(sessionId, ffi.dialogManager),
  122. ),
  123. );
  124. }
  125. // paste
  126. if (isDefaultConn &&
  127. pi.platform != kPeerPlatformAndroid &&
  128. perms['keyboard'] != false) {
  129. v.add(TTextMenu(
  130. child: Text(translate('Send clipboard keystrokes')),
  131. onPressed: () async {
  132. ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
  133. if (data != null && data.text != null) {
  134. bind.sessionInputString(
  135. sessionId: sessionId, value: data.text ?? "");
  136. }
  137. }));
  138. }
  139. // reset canvas
  140. if (isDefaultConn && isMobile) {
  141. v.add(TTextMenu(
  142. child: Text(translate('Reset canvas')),
  143. onPressed: () => ffi.cursorModel.reset()));
  144. }
  145. connectWithToken(
  146. {bool isFileTransfer = false,
  147. bool isViewCamera = false,
  148. bool isTcpTunneling = false}) {
  149. final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
  150. connect(context, id,
  151. isFileTransfer: isFileTransfer,
  152. isViewCamera: isViewCamera,
  153. isTcpTunneling: isTcpTunneling,
  154. connToken: connToken);
  155. }
  156. // transferFile
  157. if (isDefaultConn && isDesktop) {
  158. v.add(
  159. TTextMenu(
  160. child: Text(translate('Transfer file')),
  161. onPressed: () => connectWithToken(isFileTransfer: true)),
  162. );
  163. }
  164. // viewCamera
  165. if (isDefaultConn && isDesktop) {
  166. v.add(
  167. TTextMenu(
  168. child: Text(translate('View camera')),
  169. onPressed: () => connectWithToken(isViewCamera: true)),
  170. );
  171. }
  172. // tcpTunneling
  173. if (isDefaultConn && isDesktop) {
  174. v.add(
  175. TTextMenu(
  176. child: Text(translate('TCP tunneling')),
  177. onPressed: () => connectWithToken(isTcpTunneling: true)),
  178. );
  179. }
  180. // note
  181. if (isDefaultConn &&
  182. bind
  183. .sessionGetAuditServerSync(sessionId: sessionId, typ: "conn")
  184. .isNotEmpty) {
  185. v.add(
  186. TTextMenu(
  187. child: Text(translate('Note')),
  188. onPressed: () => showAuditDialog(ffi)),
  189. );
  190. }
  191. // divider
  192. if (isDefaultConn && (isDesktop || isWebDesktop)) {
  193. v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true));
  194. }
  195. // ctrlAltDel
  196. if (isDefaultConn &&
  197. !ffiModel.viewOnly &&
  198. ffiModel.keyboard &&
  199. (pi.platform == kPeerPlatformLinux || pi.sasEnabled)) {
  200. v.add(
  201. TTextMenu(
  202. child: Text('${translate("Insert Ctrl + Alt + Del")}'),
  203. onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)),
  204. );
  205. }
  206. // restart
  207. if (isDefaultConn &&
  208. perms['restart'] != false &&
  209. (pi.platform == kPeerPlatformLinux ||
  210. pi.platform == kPeerPlatformWindows ||
  211. pi.platform == kPeerPlatformMacOS)) {
  212. v.add(
  213. TTextMenu(
  214. child: Text(translate('Restart remote device')),
  215. onPressed: () =>
  216. showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager)),
  217. );
  218. }
  219. // insertLock
  220. if (isDefaultConn && !ffiModel.viewOnly && ffi.ffiModel.keyboard) {
  221. v.add(
  222. TTextMenu(
  223. child: Text(translate('Insert Lock')),
  224. onPressed: () => bind.sessionLockScreen(sessionId: sessionId)),
  225. );
  226. }
  227. // blockUserInput
  228. if (isDefaultConn &&
  229. ffi.ffiModel.keyboard &&
  230. ffi.ffiModel.permissions['block_input'] != false &&
  231. pi.platform == kPeerPlatformWindows) // privacy-mode != true ??
  232. {
  233. v.add(TTextMenu(
  234. child: Obx(() => Text(translate(
  235. '${BlockInputState.find(id).value ? 'Unb' : 'B'}lock user input'))),
  236. onPressed: () {
  237. RxBool blockInput = BlockInputState.find(id);
  238. bind.sessionToggleOption(
  239. sessionId: sessionId,
  240. value: '${blockInput.value ? 'un' : ''}block-input');
  241. blockInput.value = !blockInput.value;
  242. }));
  243. }
  244. // switchSides
  245. if (isDefaultConn &&
  246. isDesktop &&
  247. ffiModel.keyboard &&
  248. pi.platform != kPeerPlatformAndroid &&
  249. pi.platform != kPeerPlatformMacOS &&
  250. versionCmp(pi.version, '1.2.0') >= 0 &&
  251. bind.peerGetSessionsCount(id: id, connType: ffi.connType.index) == 1) {
  252. v.add(TTextMenu(
  253. child: Text(translate('Switch Sides')),
  254. onPressed: () =>
  255. showConfirmSwitchSidesDialog(sessionId, id, ffi.dialogManager)));
  256. }
  257. // refresh
  258. if (pi.version.isNotEmpty) {
  259. v.add(TTextMenu(
  260. child: Text(translate('Refresh')),
  261. onPressed: () => sessionRefreshVideo(sessionId, pi),
  262. ));
  263. }
  264. // record
  265. if (!(isDesktop || isWeb) &&
  266. (ffi.recordingModel.start || (perms["recording"] != false))) {
  267. v.add(TTextMenu(
  268. child: Row(
  269. children: [
  270. Text(translate(ffi.recordingModel.start
  271. ? 'Stop session recording'
  272. : 'Start session recording')),
  273. Padding(
  274. padding: EdgeInsets.only(left: 12),
  275. child: Icon(
  276. ffi.recordingModel.start
  277. ? Icons.pause_circle_filled
  278. : Icons.videocam_outlined,
  279. color: MyTheme.accent),
  280. )
  281. ],
  282. ),
  283. onPressed: () => ffi.recordingModel.toggle()));
  284. }
  285. // to-do:
  286. // 1. Web desktop
  287. // 2. Mobile, copy the image to the clipboard
  288. if (isDesktop) {
  289. final isScreenshotSupported = bind.sessionGetCommonSync(
  290. sessionId: sessionId, key: 'is_screenshot_supported', param: '');
  291. if ('true' == isScreenshotSupported) {
  292. v.add(TTextMenu(
  293. child: Text(ffi.ffiModel.timerScreenshot != null
  294. ? '${translate('Taking screenshot')} ...'
  295. : translate('Take screenshot')),
  296. onPressed: ffi.ffiModel.timerScreenshot != null
  297. ? null
  298. : () {
  299. if (pi.currentDisplay == kAllDisplayValue) {
  300. msgBox(
  301. sessionId,
  302. 'custom-nook-nocancel-hasclose-info',
  303. 'Take screenshot',
  304. 'screenshot-merged-screen-not-supported-tip',
  305. '',
  306. ffi.dialogManager);
  307. } else {
  308. bind.sessionTakeScreenshot(
  309. sessionId: sessionId, display: pi.currentDisplay);
  310. ffi.ffiModel.timerScreenshot =
  311. Timer(Duration(seconds: 30), () {
  312. ffi.ffiModel.timerScreenshot = null;
  313. });
  314. }
  315. },
  316. ));
  317. }
  318. }
  319. // fingerprint
  320. if (!(isDesktop || isWebDesktop)) {
  321. v.add(TTextMenu(
  322. child: Text(translate('Copy Fingerprint')),
  323. onPressed: () => onCopyFingerprint(FingerprintState.find(id).value),
  324. ));
  325. }
  326. return v;
  327. }
  328. Future<List<TRadioMenu<String>>> toolbarViewStyle(
  329. BuildContext context, String id, FFI ffi) async {
  330. final groupValue =
  331. await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
  332. void onChanged(String? value) async {
  333. if (value == null) return;
  334. bind
  335. .sessionSetViewStyle(sessionId: ffi.sessionId, value: value)
  336. .then((_) => ffi.canvasModel.updateViewStyle());
  337. }
  338. return [
  339. TRadioMenu<String>(
  340. child: Text(translate('Scale original')),
  341. value: kRemoteViewStyleOriginal,
  342. groupValue: groupValue,
  343. onChanged: onChanged),
  344. TRadioMenu<String>(
  345. child: Text(translate('Scale adaptive')),
  346. value: kRemoteViewStyleAdaptive,
  347. groupValue: groupValue,
  348. onChanged: onChanged)
  349. ];
  350. }
  351. Future<List<TRadioMenu<String>>> toolbarImageQuality(
  352. BuildContext context, String id, FFI ffi) async {
  353. final groupValue =
  354. await bind.sessionGetImageQuality(sessionId: ffi.sessionId) ?? '';
  355. onChanged(String? value) async {
  356. if (value == null) return;
  357. await bind.sessionSetImageQuality(sessionId: ffi.sessionId, value: value);
  358. }
  359. return [
  360. TRadioMenu<String>(
  361. child: Text(translate('Good image quality')),
  362. value: kRemoteImageQualityBest,
  363. groupValue: groupValue,
  364. onChanged: onChanged),
  365. TRadioMenu<String>(
  366. child: Text(translate('Balanced')),
  367. value: kRemoteImageQualityBalanced,
  368. groupValue: groupValue,
  369. onChanged: onChanged),
  370. TRadioMenu<String>(
  371. child: Text(translate('Optimize reaction time')),
  372. value: kRemoteImageQualityLow,
  373. groupValue: groupValue,
  374. onChanged: onChanged),
  375. TRadioMenu<String>(
  376. child: Text(translate('Custom')),
  377. value: kRemoteImageQualityCustom,
  378. groupValue: groupValue,
  379. onChanged: (value) {
  380. onChanged(value);
  381. customImageQualityDialog(ffi.sessionId, id, ffi);
  382. },
  383. ),
  384. ];
  385. }
  386. Future<List<TRadioMenu<String>>> toolbarCodec(
  387. BuildContext context, String id, FFI ffi) async {
  388. final sessionId = ffi.sessionId;
  389. final alternativeCodecs =
  390. await bind.sessionAlternativeCodecs(sessionId: sessionId);
  391. final groupValue = await bind.sessionGetOption(
  392. sessionId: sessionId, arg: kOptionCodecPreference) ??
  393. '';
  394. final List<bool> codecs = [];
  395. try {
  396. final Map codecsJson = jsonDecode(alternativeCodecs);
  397. final vp8 = codecsJson['vp8'] ?? false;
  398. final av1 = codecsJson['av1'] ?? false;
  399. final h264 = codecsJson['h264'] ?? false;
  400. final h265 = codecsJson['h265'] ?? false;
  401. codecs.add(vp8);
  402. codecs.add(av1);
  403. codecs.add(h264);
  404. codecs.add(h265);
  405. } catch (e) {
  406. debugPrint("Show Codec Preference err=$e");
  407. }
  408. final visible =
  409. codecs.length == 4 && (codecs[0] || codecs[1] || codecs[2] || codecs[3]);
  410. if (!visible) return [];
  411. onChanged(String? value) async {
  412. if (value == null) return;
  413. await bind.sessionPeerOption(
  414. sessionId: sessionId, name: kOptionCodecPreference, value: value);
  415. bind.sessionChangePreferCodec(sessionId: sessionId);
  416. }
  417. TRadioMenu<String> radio(String label, String value, bool enabled) {
  418. return TRadioMenu<String>(
  419. child: Text(label),
  420. value: value,
  421. groupValue: groupValue,
  422. onChanged: enabled ? onChanged : null);
  423. }
  424. var autoLabel = translate('Auto');
  425. if (groupValue == 'auto' &&
  426. ffi.qualityMonitorModel.data.codecFormat != null) {
  427. autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})';
  428. }
  429. return [
  430. radio(autoLabel, 'auto', true),
  431. if (codecs[0]) radio('VP8', 'vp8', codecs[0]),
  432. radio('VP9', 'vp9', true),
  433. if (codecs[1]) radio('AV1', 'av1', codecs[1]),
  434. if (codecs[2]) radio('H264', 'h264', codecs[2]),
  435. if (codecs[3]) radio('H265', 'h265', codecs[3]),
  436. ];
  437. }
  438. Future<List<TToggleMenu>> toolbarCursor(
  439. BuildContext context, String id, FFI ffi) async {
  440. List<TToggleMenu> v = [];
  441. final ffiModel = ffi.ffiModel;
  442. final pi = ffiModel.pi;
  443. final sessionId = ffi.sessionId;
  444. // show remote cursor
  445. if (pi.platform != kPeerPlatformAndroid &&
  446. !ffi.canvasModel.cursorEmbedded &&
  447. !pi.isWayland) {
  448. final state = ShowRemoteCursorState.find(id);
  449. final lockState = ShowRemoteCursorLockState.find(id);
  450. final enabled = !ffiModel.viewOnly;
  451. final option = 'show-remote-cursor';
  452. if (pi.currentDisplay == kAllDisplayValue ||
  453. bind.sessionIsMultiUiSession(sessionId: sessionId)) {
  454. lockState.value = false;
  455. }
  456. v.add(TToggleMenu(
  457. child: Text(translate('Show remote cursor')),
  458. value: state.value,
  459. onChanged: enabled && !lockState.value
  460. ? (value) async {
  461. if (value == null) return;
  462. await bind.sessionToggleOption(
  463. sessionId: sessionId, value: option);
  464. state.value = bind.sessionGetToggleOptionSync(
  465. sessionId: sessionId, arg: option);
  466. }
  467. : null));
  468. }
  469. // follow remote cursor
  470. if (pi.platform != kPeerPlatformAndroid &&
  471. !ffi.canvasModel.cursorEmbedded &&
  472. !pi.isWayland &&
  473. versionCmp(pi.version, "1.2.4") >= 0 &&
  474. pi.displays.length > 1 &&
  475. pi.currentDisplay != kAllDisplayValue &&
  476. !bind.sessionIsMultiUiSession(sessionId: sessionId)) {
  477. final option = 'follow-remote-cursor';
  478. final value =
  479. bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
  480. final showCursorOption = 'show-remote-cursor';
  481. final showCursorState = ShowRemoteCursorState.find(id);
  482. final showCursorLockState = ShowRemoteCursorLockState.find(id);
  483. final showCursorEnabled = bind.sessionGetToggleOptionSync(
  484. sessionId: sessionId, arg: showCursorOption);
  485. showCursorLockState.value = value;
  486. if (value && !showCursorEnabled) {
  487. await bind.sessionToggleOption(
  488. sessionId: sessionId, value: showCursorOption);
  489. showCursorState.value = bind.sessionGetToggleOptionSync(
  490. sessionId: sessionId, arg: showCursorOption);
  491. }
  492. v.add(TToggleMenu(
  493. child: Text(translate('Follow remote cursor')),
  494. value: value,
  495. onChanged: (value) async {
  496. if (value == null) return;
  497. await bind.sessionToggleOption(sessionId: sessionId, value: option);
  498. value = bind.sessionGetToggleOptionSync(
  499. sessionId: sessionId, arg: option);
  500. showCursorLockState.value = value;
  501. if (!showCursorEnabled) {
  502. await bind.sessionToggleOption(
  503. sessionId: sessionId, value: showCursorOption);
  504. showCursorState.value = bind.sessionGetToggleOptionSync(
  505. sessionId: sessionId, arg: showCursorOption);
  506. }
  507. }));
  508. }
  509. // follow remote window focus
  510. if (pi.platform != kPeerPlatformAndroid &&
  511. !ffi.canvasModel.cursorEmbedded &&
  512. !pi.isWayland &&
  513. versionCmp(pi.version, "1.2.4") >= 0 &&
  514. pi.displays.length > 1 &&
  515. pi.currentDisplay != kAllDisplayValue &&
  516. !bind.sessionIsMultiUiSession(sessionId: sessionId)) {
  517. final option = 'follow-remote-window';
  518. final value =
  519. bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
  520. v.add(TToggleMenu(
  521. child: Text(translate('Follow remote window focus')),
  522. value: value,
  523. onChanged: (value) async {
  524. if (value == null) return;
  525. await bind.sessionToggleOption(sessionId: sessionId, value: option);
  526. value = bind.sessionGetToggleOptionSync(
  527. sessionId: sessionId, arg: option);
  528. }));
  529. }
  530. // zoom cursor
  531. final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? '';
  532. if (!isMobile &&
  533. pi.platform != kPeerPlatformAndroid &&
  534. viewStyle != kRemoteViewStyleOriginal) {
  535. final option = 'zoom-cursor';
  536. final peerState = PeerBoolOption.find(id, option);
  537. v.add(TToggleMenu(
  538. child: Text(translate('Zoom cursor')),
  539. value: peerState.value,
  540. onChanged: (value) async {
  541. if (value == null) return;
  542. await bind.sessionToggleOption(sessionId: sessionId, value: option);
  543. peerState.value =
  544. bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
  545. },
  546. ));
  547. }
  548. return v;
  549. }
  550. Future<List<TToggleMenu>> toolbarDisplayToggle(
  551. BuildContext context, String id, FFI ffi) async {
  552. List<TToggleMenu> v = [];
  553. final ffiModel = ffi.ffiModel;
  554. final pi = ffiModel.pi;
  555. final perms = ffiModel.permissions;
  556. final sessionId = ffi.sessionId;
  557. final isDefaultConn = ffi.connType == ConnType.defaultConn;
  558. // show quality monitor
  559. final option = 'show-quality-monitor';
  560. v.add(TToggleMenu(
  561. value: bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option),
  562. onChanged: (value) async {
  563. if (value == null) return;
  564. await bind.sessionToggleOption(sessionId: sessionId, value: option);
  565. ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
  566. },
  567. child: Text(translate('Show quality monitor'))));
  568. // mute
  569. if (isDefaultConn && perms['audio'] != false) {
  570. final option = 'disable-audio';
  571. final value =
  572. bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
  573. v.add(TToggleMenu(
  574. value: value,
  575. onChanged: (value) {
  576. if (value == null) return;
  577. bind.sessionToggleOption(sessionId: sessionId, value: option);
  578. },
  579. child: Text(translate('Mute'))));
  580. }
  581. // file copy and paste
  582. // If the version is less than 1.2.4, file copy and paste is supported on Windows only.
  583. final isSupportIfPeer_1_2_3 = versionCmp(pi.version, '1.2.4') < 0 &&
  584. isWindows &&
  585. pi.platform == kPeerPlatformWindows;
  586. // If the version is 1.2.4 or later, file copy and paste is supported when kPlatformAdditionsHasFileClipboard is set.
  587. final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 &&
  588. bind.mainHasFileClipboard() &&
  589. pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard);
  590. if (isDefaultConn &&
  591. ffiModel.keyboard &&
  592. perms['file'] != false &&
  593. (isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
  594. final enabled = !ffiModel.viewOnly;
  595. final value = bind.sessionGetToggleOptionSync(
  596. sessionId: sessionId, arg: kOptionEnableFileCopyPaste);
  597. v.add(TToggleMenu(
  598. value: value,
  599. onChanged: enabled
  600. ? (value) {
  601. if (value == null) return;
  602. bind.sessionToggleOption(
  603. sessionId: sessionId, value: kOptionEnableFileCopyPaste);
  604. }
  605. : null,
  606. child: Text(translate('Enable file copy and paste'))));
  607. }
  608. // disable clipboard
  609. if (isDefaultConn && ffiModel.keyboard && perms['clipboard'] != false) {
  610. final enabled = !ffiModel.viewOnly;
  611. final option = 'disable-clipboard';
  612. var value =
  613. bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
  614. if (ffiModel.viewOnly) value = true;
  615. v.add(TToggleMenu(
  616. value: value,
  617. onChanged: enabled
  618. ? (value) {
  619. if (value == null) return;
  620. bind.sessionToggleOption(sessionId: sessionId, value: option);
  621. }
  622. : null,
  623. child: Text(translate('Disable clipboard'))));
  624. }
  625. // lock after session end
  626. if (isDefaultConn && ffiModel.keyboard && !ffiModel.isPeerAndroid) {
  627. final enabled = !ffiModel.viewOnly;
  628. final option = 'lock-after-session-end';
  629. final value =
  630. bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
  631. v.add(TToggleMenu(
  632. value: value,
  633. onChanged: enabled
  634. ? (value) {
  635. if (value == null) return;
  636. bind.sessionToggleOption(sessionId: sessionId, value: option);
  637. }
  638. : null,
  639. child: Text(translate('Lock after session end'))));
  640. }
  641. if (pi.isSupportMultiDisplay &&
  642. PrivacyModeState.find(id).isEmpty &&
  643. pi.displaysCount.value > 1 &&
  644. bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
  645. final value =
  646. bind.sessionGetDisplaysAsIndividualWindows(sessionId: ffi.sessionId) ==
  647. 'Y';
  648. v.add(TToggleMenu(
  649. value: value,
  650. onChanged: (value) {
  651. if (value == null) return;
  652. bind.sessionSetDisplaysAsIndividualWindows(
  653. sessionId: sessionId, value: value ? 'Y' : 'N');
  654. },
  655. child: Text(translate('Show displays as individual windows'))));
  656. }
  657. final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
  658. if (pi.isSupportMultiDisplay && isMultiScreens) {
  659. final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
  660. sessionId: ffi.sessionId) ==
  661. 'Y';
  662. v.add(TToggleMenu(
  663. value: value,
  664. onChanged: (value) {
  665. if (value == null) return;
  666. bind.sessionSetUseAllMyDisplaysForTheRemoteSession(
  667. sessionId: sessionId, value: value ? 'Y' : 'N');
  668. },
  669. child: Text(translate('Use all my displays for the remote session'))));
  670. }
  671. // 444
  672. final codec_format = ffi.qualityMonitorModel.data.codecFormat;
  673. if (versionCmp(pi.version, "1.2.4") >= 0 &&
  674. (codec_format == "AV1" || codec_format == "VP9")) {
  675. final option = 'i444';
  676. final value =
  677. bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
  678. v.add(TToggleMenu(
  679. value: value,
  680. onChanged: (value) async {
  681. if (value == null) return;
  682. await bind.sessionToggleOption(sessionId: sessionId, value: option);
  683. bind.sessionChangePreferCodec(sessionId: sessionId);
  684. },
  685. child: Text(translate('True color (4:4:4)'))));
  686. }
  687. if (isDefaultConn && isMobile) {
  688. v.addAll(toolbarKeyboardToggles(ffi));
  689. }
  690. // view mode (mobile only, desktop is in keyboard menu)
  691. if (isDefaultConn && isMobile && versionCmp(pi.version, '1.2.0') >= 0) {
  692. v.add(TToggleMenu(
  693. value: ffiModel.viewOnly,
  694. onChanged: (value) async {
  695. if (value == null) return;
  696. await bind.sessionToggleOption(
  697. sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
  698. ffiModel.setViewOnly(id, value);
  699. },
  700. child: Text(translate('View Mode'))));
  701. }
  702. return v;
  703. }
  704. var togglePrivacyModeTime = DateTime.now().subtract(const Duration(hours: 1));
  705. List<TToggleMenu> toolbarPrivacyMode(
  706. RxString privacyModeState, BuildContext context, String id, FFI ffi) {
  707. final ffiModel = ffi.ffiModel;
  708. final pi = ffiModel.pi;
  709. final sessionId = ffi.sessionId;
  710. getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc) {
  711. final enabled = !ffi.ffiModel.viewOnly;
  712. return TToggleMenu(
  713. value: privacyModeState.isNotEmpty,
  714. onChanged: enabled
  715. ? (value) {
  716. if (value == null) return;
  717. if (ffiModel.pi.currentDisplay != 0 &&
  718. ffiModel.pi.currentDisplay != kAllDisplayValue) {
  719. msgBox(
  720. sessionId,
  721. 'custom-nook-nocancel-hasclose',
  722. 'info',
  723. 'Please switch to Display 1 first',
  724. '',
  725. ffi.dialogManager);
  726. return;
  727. }
  728. final option = 'privacy-mode';
  729. toggleFunc(sessionId, option);
  730. }
  731. : null,
  732. child: Text(translate('Privacy mode')));
  733. }
  734. final privacyModeImpls =
  735. pi.platformAdditions[kPlatformAdditionsSupportedPrivacyModeImpl]
  736. as List<dynamic>?;
  737. if (privacyModeImpls == null) {
  738. return [
  739. getDefaultMenu((sid, opt) async {
  740. bind.sessionToggleOption(sessionId: sid, value: opt);
  741. togglePrivacyModeTime = DateTime.now();
  742. })
  743. ];
  744. }
  745. if (privacyModeImpls.isEmpty) {
  746. return [];
  747. }
  748. if (privacyModeImpls.length == 1) {
  749. final implKey = (privacyModeImpls[0] as List<dynamic>)[0] as String;
  750. return [
  751. getDefaultMenu((sid, opt) async {
  752. bind.sessionTogglePrivacyMode(
  753. sessionId: sid, implKey: implKey, on: privacyModeState.isEmpty);
  754. togglePrivacyModeTime = DateTime.now();
  755. })
  756. ];
  757. } else {
  758. return privacyModeImpls.map((e) {
  759. final implKey = (e as List<dynamic>)[0] as String;
  760. final implName = (e)[1] as String;
  761. return TToggleMenu(
  762. child: Text(translate(implName)),
  763. value: privacyModeState.value == implKey,
  764. onChanged: (value) {
  765. if (value == null) return;
  766. togglePrivacyModeTime = DateTime.now();
  767. bind.sessionTogglePrivacyMode(
  768. sessionId: sessionId, implKey: implKey, on: value);
  769. });
  770. }).toList();
  771. }
  772. }
  773. List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
  774. final ffiModel = ffi.ffiModel;
  775. final pi = ffiModel.pi;
  776. final sessionId = ffi.sessionId;
  777. List<TToggleMenu> v = [];
  778. // swap key
  779. if (ffiModel.keyboard &&
  780. ((isMacOS && pi.platform != kPeerPlatformMacOS) ||
  781. (!isMacOS && pi.platform == kPeerPlatformMacOS))) {
  782. final option = 'allow_swap_key';
  783. final value =
  784. bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
  785. onChanged(bool? value) {
  786. if (value == null) return;
  787. bind.sessionToggleOption(sessionId: sessionId, value: option);
  788. }
  789. final enabled = !ffi.ffiModel.viewOnly;
  790. v.add(TToggleMenu(
  791. value: value,
  792. onChanged: enabled ? onChanged : null,
  793. child: Text(translate('Swap control-command key'))));
  794. }
  795. // reverse mouse wheel
  796. if (ffiModel.keyboard) {
  797. var optionValue =
  798. bind.sessionGetReverseMouseWheelSync(sessionId: sessionId) ?? '';
  799. if (optionValue == '') {
  800. optionValue = bind.mainGetUserDefaultOption(key: kKeyReverseMouseWheel);
  801. }
  802. onChanged(bool? value) async {
  803. if (value == null) return;
  804. await bind.sessionSetReverseMouseWheel(
  805. sessionId: sessionId, value: value ? 'Y' : 'N');
  806. }
  807. final enabled = !ffi.ffiModel.viewOnly;
  808. v.add(TToggleMenu(
  809. value: optionValue == 'Y',
  810. onChanged: enabled ? onChanged : null,
  811. child: Text(translate('Reverse mouse wheel'))));
  812. }
  813. // swap left right mouse
  814. if (ffiModel.keyboard) {
  815. final option = 'swap-left-right-mouse';
  816. final value =
  817. bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
  818. onChanged(bool? value) {
  819. if (value == null) return;
  820. bind.sessionToggleOption(sessionId: sessionId, value: option);
  821. }
  822. final enabled = !ffi.ffiModel.viewOnly;
  823. v.add(TToggleMenu(
  824. value: value,
  825. onChanged: enabled ? onChanged : null,
  826. child: Text(translate('swap-left-right-mouse'))));
  827. }
  828. return v;
  829. }
  830. bool showVirtualDisplayMenu(FFI ffi) {
  831. if (ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
  832. return false;
  833. }
  834. if (!ffi.ffiModel.pi.isInstalled) {
  835. return false;
  836. }
  837. if (ffi.ffiModel.pi.isRustDeskIdd || ffi.ffiModel.pi.isAmyuniIdd) {
  838. return true;
  839. }
  840. return false;
  841. }
  842. List<Widget> getVirtualDisplayMenuChildren(
  843. FFI ffi, String id, VoidCallback? clickCallBack) {
  844. if (!showVirtualDisplayMenu(ffi)) {
  845. return [];
  846. }
  847. final pi = ffi.ffiModel.pi;
  848. final privacyModeState = PrivacyModeState.find(id);
  849. if (pi.isRustDeskIdd) {
  850. final virtualDisplays = ffi.ffiModel.pi.RustDeskVirtualDisplays;
  851. final children = <Widget>[];
  852. for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
  853. children.add(Obx(() => CkbMenuButton(
  854. value: virtualDisplays.contains(i + 1),
  855. onChanged: privacyModeState.isNotEmpty
  856. ? null
  857. : (bool? value) async {
  858. if (value != null) {
  859. bind.sessionToggleVirtualDisplay(
  860. sessionId: ffi.sessionId, index: i + 1, on: value);
  861. clickCallBack?.call();
  862. }
  863. },
  864. child: Text('${translate('Virtual display')} ${i + 1}'),
  865. ffi: ffi,
  866. )));
  867. }
  868. children.add(Divider());
  869. children.add(Obx(() => MenuButton(
  870. onPressed: privacyModeState.isNotEmpty
  871. ? null
  872. : () {
  873. bind.sessionToggleVirtualDisplay(
  874. sessionId: ffi.sessionId,
  875. index: kAllVirtualDisplay,
  876. on: false);
  877. clickCallBack?.call();
  878. },
  879. ffi: ffi,
  880. child: Text(translate('Plug out all')),
  881. )));
  882. return children;
  883. }
  884. if (pi.isAmyuniIdd) {
  885. final count = ffi.ffiModel.pi.amyuniVirtualDisplayCount;
  886. final children = <Widget>[
  887. Obx(() => Row(
  888. children: [
  889. TextButton(
  890. onPressed: privacyModeState.isNotEmpty || count == 0
  891. ? null
  892. : () {
  893. bind.sessionToggleVirtualDisplay(
  894. sessionId: ffi.sessionId, index: 0, on: false);
  895. clickCallBack?.call();
  896. },
  897. child: Icon(Icons.remove),
  898. ),
  899. Text(count.toString()),
  900. TextButton(
  901. onPressed: privacyModeState.isNotEmpty || count == 4
  902. ? null
  903. : () {
  904. bind.sessionToggleVirtualDisplay(
  905. sessionId: ffi.sessionId, index: 0, on: true);
  906. clickCallBack?.call();
  907. },
  908. child: Icon(Icons.add),
  909. ),
  910. ],
  911. )),
  912. Divider(),
  913. Obx(() => MenuButton(
  914. onPressed: privacyModeState.isNotEmpty || count == 0
  915. ? null
  916. : () {
  917. bind.sessionToggleVirtualDisplay(
  918. sessionId: ffi.sessionId,
  919. index: kAllVirtualDisplay,
  920. on: false);
  921. clickCallBack?.call();
  922. },
  923. ffi: ffi,
  924. child: Text(translate('Plug out all')),
  925. )),
  926. ];
  927. return children;
  928. }
  929. return [];
  930. }