toolbar.dart 32 KB

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