toolbar.dart 30 KB

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