peer_card.dart 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429
  1. import 'package:bot_toast/bot_toast.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:flutter_hbb/common/widgets/dialog.dart';
  5. import 'package:flutter_hbb/consts.dart';
  6. import 'package:flutter_hbb/models/peer_tab_model.dart';
  7. import 'package:flutter_hbb/models/state_model.dart';
  8. import 'package:get/get.dart';
  9. import 'package:provider/provider.dart';
  10. import '../../common.dart';
  11. import '../../common/formatter/id_formatter.dart';
  12. import '../../models/peer_model.dart';
  13. import '../../models/platform_model.dart';
  14. import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
  15. import '../../desktop/widgets/popup_menu.dart';
  16. import 'dart:math' as math;
  17. typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
  18. Function(BuildContext);
  19. enum PeerUiType { grid, tile, list }
  20. final peerCardUiType = PeerUiType.grid.obs;
  21. bool? hideUsernameOnCard;
  22. class _PeerCard extends StatefulWidget {
  23. final Peer peer;
  24. final PeerTabIndex tab;
  25. final Function(BuildContext, String) connect;
  26. final PopupMenuEntryBuilder popupMenuEntryBuilder;
  27. const _PeerCard(
  28. {required this.peer,
  29. required this.tab,
  30. required this.connect,
  31. required this.popupMenuEntryBuilder,
  32. Key? key})
  33. : super(key: key);
  34. @override
  35. _PeerCardState createState() => _PeerCardState();
  36. }
  37. /// State for the connection page.
  38. class _PeerCardState extends State<_PeerCard>
  39. with AutomaticKeepAliveClientMixin {
  40. var _menuPos = RelativeRect.fill;
  41. final double _cardRadius = 16;
  42. final double _tileRadius = 5;
  43. final double _borderWidth = 2;
  44. @override
  45. Widget build(BuildContext context) {
  46. super.build(context);
  47. return Obx(() =>
  48. stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape());
  49. }
  50. Widget gestureDetector({required Widget child}) {
  51. final PeerTabModel peerTabModel = Provider.of(context);
  52. final peer = super.widget.peer;
  53. return GestureDetector(
  54. onDoubleTap: peerTabModel.multiSelectionMode
  55. ? null
  56. : () => widget.connect(context, peer.id),
  57. onTap: () {
  58. if (peerTabModel.multiSelectionMode) {
  59. peerTabModel.select(peer);
  60. } else {
  61. if (isMobile) {
  62. widget.connect(context, peer.id);
  63. } else {
  64. peerTabModel.select(peer);
  65. }
  66. }
  67. },
  68. onLongPress: () => peerTabModel.select(peer),
  69. child: child);
  70. }
  71. Widget _buildPortrait() {
  72. final peer = super.widget.peer;
  73. return Card(
  74. margin: EdgeInsets.symmetric(horizontal: 2),
  75. child: gestureDetector(
  76. child: Container(
  77. padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
  78. child: _buildPeerTile(context, peer, null)),
  79. ));
  80. }
  81. Widget _buildLandscape() {
  82. final peer = super.widget.peer;
  83. var deco = Rx<BoxDecoration?>(
  84. BoxDecoration(
  85. border: Border.all(color: Colors.transparent, width: _borderWidth),
  86. borderRadius: BorderRadius.circular(
  87. peerCardUiType.value == PeerUiType.grid ? _cardRadius : _tileRadius,
  88. ),
  89. ),
  90. );
  91. return MouseRegion(
  92. onEnter: (evt) {
  93. deco.value = BoxDecoration(
  94. border: Border.all(
  95. color: Theme.of(context).colorScheme.primary,
  96. width: _borderWidth),
  97. borderRadius: BorderRadius.circular(
  98. peerCardUiType.value == PeerUiType.grid ? _cardRadius : _tileRadius,
  99. ),
  100. );
  101. },
  102. onExit: (evt) {
  103. deco.value = BoxDecoration(
  104. border: Border.all(color: Colors.transparent, width: _borderWidth),
  105. borderRadius: BorderRadius.circular(
  106. peerCardUiType.value == PeerUiType.grid ? _cardRadius : _tileRadius,
  107. ),
  108. );
  109. },
  110. child: gestureDetector(
  111. child: Obx(() => peerCardUiType.value == PeerUiType.grid
  112. ? _buildPeerCard(context, peer, deco)
  113. : _buildPeerTile(context, peer, deco))),
  114. );
  115. }
  116. makeChild(bool isPortrait, Peer peer) {
  117. final name = hideUsernameOnCard == true
  118. ? peer.hostname
  119. : '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
  120. final greyStyle = TextStyle(
  121. fontSize: 11,
  122. color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
  123. return Row(
  124. mainAxisSize: MainAxisSize.max,
  125. children: [
  126. Container(
  127. decoration: BoxDecoration(
  128. color: str2color('${peer.id}${peer.platform}', 0x7f),
  129. borderRadius: isPortrait
  130. ? BorderRadius.circular(_tileRadius)
  131. : BorderRadius.only(
  132. topLeft: Radius.circular(_tileRadius),
  133. bottomLeft: Radius.circular(_tileRadius),
  134. ),
  135. ),
  136. alignment: Alignment.center,
  137. width: isPortrait ? 50 : 42,
  138. height: isPortrait ? 50 : null,
  139. child: Stack(
  140. children: [
  141. getPlatformImage(peer.platform, size: isPortrait ? 38 : 30)
  142. .paddingAll(6),
  143. if (_shouldBuildPasswordIcon(peer))
  144. Positioned(
  145. top: 1,
  146. left: 1,
  147. child: Icon(Icons.key, size: 6, color: Colors.white),
  148. ),
  149. ],
  150. )),
  151. Expanded(
  152. child: Container(
  153. decoration: BoxDecoration(
  154. color: Theme.of(context).colorScheme.background,
  155. borderRadius: BorderRadius.only(
  156. topRight: Radius.circular(_tileRadius),
  157. bottomRight: Radius.circular(_tileRadius),
  158. ),
  159. ),
  160. child: Row(
  161. children: [
  162. Expanded(
  163. child: Column(
  164. children: [
  165. Row(children: [
  166. getOnline(isPortrait ? 4 : 8, peer.online),
  167. Expanded(
  168. child: Text(
  169. peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
  170. overflow: TextOverflow.ellipsis,
  171. style: Theme.of(context).textTheme.titleSmall,
  172. )),
  173. ]).marginOnly(top: isPortrait ? 0 : 2),
  174. Align(
  175. alignment: Alignment.centerLeft,
  176. child: Text(
  177. name,
  178. style: isPortrait ? null : greyStyle,
  179. textAlign: TextAlign.start,
  180. overflow: TextOverflow.ellipsis,
  181. ),
  182. ),
  183. ],
  184. ).marginOnly(top: 2),
  185. ),
  186. isPortrait
  187. ? checkBoxOrActionMorePortrait(peer)
  188. : checkBoxOrActionMoreLandscape(peer, isTile: true),
  189. ],
  190. ).paddingOnly(left: 10.0, top: 3.0),
  191. ),
  192. )
  193. ],
  194. );
  195. }
  196. Widget _buildPeerTile(
  197. BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
  198. hideUsernameOnCard ??=
  199. bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
  200. final colors = _frontN(peer.tags, 25)
  201. .map((e) => gFFI.abModel.getCurrentAbTagColor(e))
  202. .toList();
  203. return Tooltip(
  204. message: !(isDesktop || isWebDesktop)
  205. ? ''
  206. : peer.tags.isNotEmpty
  207. ? '${translate('Tags')}: ${peer.tags.join(', ')}'
  208. : '',
  209. child: Stack(children: [
  210. Obx(
  211. () => deco == null
  212. ? makeChild(stateGlobal.isPortrait.isTrue, peer)
  213. : Container(
  214. foregroundDecoration: deco.value,
  215. child: makeChild(stateGlobal.isPortrait.isTrue, peer),
  216. ),
  217. ),
  218. if (colors.isNotEmpty)
  219. Obx(() => Positioned(
  220. top: 2,
  221. right: stateGlobal.isPortrait.isTrue ? 20 : 10,
  222. child: CustomPaint(
  223. painter: TagPainter(radius: 3, colors: colors),
  224. ),
  225. ))
  226. ]),
  227. );
  228. }
  229. Widget _buildPeerCard(
  230. BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
  231. hideUsernameOnCard ??=
  232. bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
  233. final name = hideUsernameOnCard == true
  234. ? peer.hostname
  235. : '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
  236. final child = Card(
  237. color: Colors.transparent,
  238. elevation: 0,
  239. margin: EdgeInsets.zero,
  240. // to-do: memory leak here, more investigation needed.
  241. // Continious rebuilds of `Obx()` will cause memory leak here.
  242. // The simple demo does not have this issue.
  243. child: Obx(
  244. () => Container(
  245. foregroundDecoration: deco.value,
  246. child: ClipRRect(
  247. borderRadius: BorderRadius.circular(_cardRadius - _borderWidth),
  248. child: Column(
  249. mainAxisSize: MainAxisSize.min,
  250. mainAxisAlignment: MainAxisAlignment.center,
  251. children: [
  252. Expanded(
  253. child: Container(
  254. color: str2color('${peer.id}${peer.platform}', 0x7f),
  255. child: Row(
  256. children: [
  257. Expanded(
  258. child: Column(
  259. crossAxisAlignment: CrossAxisAlignment.center,
  260. children: [
  261. Container(
  262. padding: const EdgeInsets.all(6),
  263. child:
  264. getPlatformImage(peer.platform, size: 60),
  265. ).marginOnly(top: 4),
  266. Row(
  267. children: [
  268. Expanded(
  269. child: Tooltip(
  270. message: name,
  271. waitDuration: const Duration(seconds: 1),
  272. child: Text(
  273. name,
  274. style: const TextStyle(
  275. color: Colors.white70,
  276. fontSize: 12),
  277. textAlign: TextAlign.center,
  278. overflow: TextOverflow.ellipsis,
  279. ),
  280. ),
  281. ),
  282. ],
  283. ),
  284. ],
  285. ).paddingAll(4.0),
  286. ),
  287. ],
  288. ),
  289. ),
  290. ),
  291. Container(
  292. color: Theme.of(context).colorScheme.background,
  293. child: Row(
  294. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  295. children: [
  296. Expanded(
  297. child: Row(children: [
  298. getOnline(8, peer.online),
  299. Expanded(
  300. child: Text(
  301. peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
  302. overflow: TextOverflow.ellipsis,
  303. style: Theme.of(context).textTheme.titleSmall,
  304. )),
  305. ]).paddingSymmetric(vertical: 8)),
  306. checkBoxOrActionMoreLandscape(peer, isTile: false),
  307. ],
  308. ).paddingSymmetric(horizontal: 12.0),
  309. )
  310. ],
  311. ),
  312. ),
  313. ),
  314. ),
  315. );
  316. final colors = _frontN(peer.tags, 25)
  317. .map((e) => gFFI.abModel.getCurrentAbTagColor(e))
  318. .toList();
  319. return Tooltip(
  320. message: peer.tags.isNotEmpty
  321. ? '${translate('Tags')}: ${peer.tags.join(', ')}'
  322. : '',
  323. child: Stack(children: [
  324. child,
  325. if (_shouldBuildPasswordIcon(peer))
  326. Positioned(
  327. top: 4,
  328. left: 12,
  329. child: Icon(Icons.key, size: 12, color: Colors.white),
  330. ),
  331. if (colors.isNotEmpty)
  332. Positioned(
  333. top: 4,
  334. right: 12,
  335. child: CustomPaint(
  336. painter: TagPainter(radius: 4, colors: colors),
  337. ),
  338. )
  339. ]),
  340. );
  341. }
  342. List _frontN<T>(List list, int n) {
  343. if (list.length <= n) {
  344. return list;
  345. } else {
  346. return list.sublist(0, n);
  347. }
  348. }
  349. Widget checkBoxOrActionMorePortrait(Peer peer) {
  350. final PeerTabModel peerTabModel = Provider.of(context);
  351. final selected = peerTabModel.isPeerSelected(peer.id);
  352. if (peerTabModel.multiSelectionMode) {
  353. return Padding(
  354. padding: const EdgeInsets.all(12),
  355. child: selected
  356. ? Icon(
  357. Icons.check_box,
  358. color: MyTheme.accent,
  359. )
  360. : Icon(Icons.check_box_outline_blank),
  361. );
  362. } else {
  363. return InkWell(
  364. child: const Padding(
  365. padding: EdgeInsets.all(12), child: Icon(Icons.more_vert)),
  366. onTapDown: (e) {
  367. final x = e.globalPosition.dx;
  368. final y = e.globalPosition.dy;
  369. _menuPos = RelativeRect.fromLTRB(x, y, x, y);
  370. },
  371. onTap: () {
  372. _showPeerMenu(peer.id);
  373. });
  374. }
  375. }
  376. Widget checkBoxOrActionMoreLandscape(Peer peer, {required bool isTile}) {
  377. final PeerTabModel peerTabModel = Provider.of(context);
  378. final selected = peerTabModel.isPeerSelected(peer.id);
  379. if (peerTabModel.multiSelectionMode) {
  380. final icon = selected
  381. ? Icon(
  382. Icons.check_box,
  383. color: MyTheme.accent,
  384. )
  385. : Icon(Icons.check_box_outline_blank);
  386. bool last = peerTabModel.isShiftDown && peer.id == peerTabModel.lastId;
  387. double right = isTile ? 4 : 0;
  388. if (last) {
  389. return Container(
  390. decoration: BoxDecoration(
  391. border: Border.all(color: MyTheme.accent, width: 1)),
  392. child: icon,
  393. ).marginOnly(right: right);
  394. } else {
  395. return icon.marginOnly(right: right);
  396. }
  397. } else {
  398. return _actionMore(peer);
  399. }
  400. }
  401. Widget _actionMore(Peer peer) => Listener(
  402. onPointerDown: (e) {
  403. final x = e.position.dx;
  404. final y = e.position.dy;
  405. _menuPos = RelativeRect.fromLTRB(x, y, x, y);
  406. },
  407. onPointerUp: (_) => _showPeerMenu(peer.id),
  408. child: build_more(context));
  409. bool _shouldBuildPasswordIcon(Peer peer) {
  410. if (gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index) return false;
  411. if (gFFI.abModel.current.isPersonal()) return false;
  412. return peer.password.isNotEmpty;
  413. }
  414. /// Show the peer menu and handle user's choice.
  415. /// User might remove the peer or send a file to the peer.
  416. void _showPeerMenu(String id) async {
  417. await mod_menu.showMenu(
  418. context: context,
  419. position: _menuPos,
  420. items: await super.widget.popupMenuEntryBuilder(context),
  421. elevation: 8,
  422. );
  423. }
  424. @override
  425. bool get wantKeepAlive => true;
  426. }
  427. abstract class BasePeerCard extends StatelessWidget {
  428. final Peer peer;
  429. final PeerTabIndex tab;
  430. final EdgeInsets? menuPadding;
  431. BasePeerCard(
  432. {required this.peer, required this.tab, this.menuPadding, Key? key})
  433. : super(key: key);
  434. @override
  435. Widget build(BuildContext context) {
  436. return _PeerCard(
  437. peer: peer,
  438. tab: tab,
  439. connect: (BuildContext context, String id) =>
  440. connectInPeerTab(context, peer, tab),
  441. popupMenuEntryBuilder: _buildPopupMenuEntry,
  442. );
  443. }
  444. Future<List<mod_menu.PopupMenuEntry<String>>> _buildPopupMenuEntry(
  445. BuildContext context) async =>
  446. (await _buildMenuItems(context))
  447. .map((e) => e.build(
  448. context,
  449. const MenuConfig(
  450. commonColor: CustomPopupMenuTheme.commonColor,
  451. height: CustomPopupMenuTheme.height,
  452. dividerHeight: CustomPopupMenuTheme.dividerHeight)))
  453. .expand((i) => i)
  454. .toList();
  455. @protected
  456. Future<List<MenuEntryBase<String>>> _buildMenuItems(BuildContext context);
  457. MenuEntryBase<String> _connectCommonAction(
  458. BuildContext context,
  459. String title, {
  460. bool isFileTransfer = false,
  461. bool isTcpTunneling = false,
  462. bool isRDP = false,
  463. }) {
  464. return MenuEntryButton<String>(
  465. childBuilder: (TextStyle? style) => Text(
  466. title,
  467. style: style,
  468. ),
  469. proc: () {
  470. connectInPeerTab(
  471. context,
  472. peer,
  473. tab,
  474. isFileTransfer: isFileTransfer,
  475. isTcpTunneling: isTcpTunneling,
  476. isRDP: isRDP,
  477. );
  478. },
  479. padding: menuPadding,
  480. dismissOnClicked: true,
  481. );
  482. }
  483. @protected
  484. MenuEntryBase<String> _connectAction(BuildContext context) {
  485. return _connectCommonAction(
  486. context,
  487. (peer.alias.isEmpty
  488. ? translate('Connect')
  489. : '${translate('Connect')} ${peer.id}'),
  490. );
  491. }
  492. @protected
  493. MenuEntryBase<String> _transferFileAction(BuildContext context) {
  494. return _connectCommonAction(
  495. context,
  496. translate('Transfer file'),
  497. isFileTransfer: true,
  498. );
  499. }
  500. @protected
  501. MenuEntryBase<String> _tcpTunnelingAction(BuildContext context) {
  502. return _connectCommonAction(
  503. context,
  504. translate('TCP tunneling'),
  505. isTcpTunneling: true,
  506. );
  507. }
  508. @protected
  509. MenuEntryBase<String> _rdpAction(BuildContext context, String id) {
  510. return MenuEntryButton<String>(
  511. childBuilder: (TextStyle? style) => Container(
  512. alignment: AlignmentDirectional.center,
  513. height: CustomPopupMenuTheme.height,
  514. child: Row(
  515. children: [
  516. Text(
  517. translate('RDP'),
  518. style: style,
  519. ),
  520. Expanded(
  521. child: Align(
  522. alignment: Alignment.centerRight,
  523. child: Transform.scale(
  524. scale: 0.8,
  525. child: IconButton(
  526. icon: const Icon(Icons.edit),
  527. padding: EdgeInsets.zero,
  528. onPressed: () {
  529. if (Navigator.canPop(context)) {
  530. Navigator.pop(context);
  531. }
  532. _rdpDialog(id);
  533. },
  534. )),
  535. ))
  536. ],
  537. )),
  538. proc: () {
  539. connectInPeerTab(context, peer, tab, isRDP: true);
  540. },
  541. padding: menuPadding,
  542. dismissOnClicked: true,
  543. );
  544. }
  545. @protected
  546. MenuEntryBase<String> _wolAction(String id) {
  547. return MenuEntryButton<String>(
  548. childBuilder: (TextStyle? style) => Text(
  549. translate('WOL'),
  550. style: style,
  551. ),
  552. proc: () {
  553. bind.mainWol(id: id);
  554. },
  555. padding: menuPadding,
  556. dismissOnClicked: true,
  557. );
  558. }
  559. /// Only available on Windows.
  560. @protected
  561. MenuEntryBase<String> _createShortCutAction(String id) {
  562. return MenuEntryButton<String>(
  563. childBuilder: (TextStyle? style) => Text(
  564. translate('Create desktop shortcut'),
  565. style: style,
  566. ),
  567. proc: () {
  568. bind.mainCreateShortcut(id: id);
  569. showToast(translate('Successful'));
  570. },
  571. padding: menuPadding,
  572. dismissOnClicked: true,
  573. );
  574. }
  575. Future<MenuEntryBase<String>> _openNewConnInAction(
  576. String id, String label, String key) async {
  577. return MenuEntrySwitch<String>(
  578. switchType: SwitchType.scheckbox,
  579. text: translate(label),
  580. getter: () async => mainGetPeerBoolOptionSync(id, key),
  581. setter: (bool v) async {
  582. await bind.mainSetPeerOption(
  583. id: id, key: key, value: bool2option(key, v));
  584. showToast(translate('Successful'));
  585. },
  586. padding: menuPadding,
  587. dismissOnClicked: true,
  588. );
  589. }
  590. _openInTabsAction(String id) async =>
  591. await _openNewConnInAction(id, 'Open in New Tab', kOptionOpenInTabs);
  592. _openInWindowsAction(String id) async => await _openNewConnInAction(
  593. id, 'Open in new window', kOptionOpenInWindows);
  594. // ignore: unused_element
  595. _openNewConnInOptAction(String id) async =>
  596. mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
  597. ? await _openInWindowsAction(id)
  598. : await _openInTabsAction(id);
  599. @protected
  600. Future<bool> _isForceAlwaysRelay(String id) async {
  601. return option2bool(kOptionForceAlwaysRelay,
  602. (await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay)));
  603. }
  604. @protected
  605. Future<MenuEntryBase<String>> _forceAlwaysRelayAction(String id) async {
  606. return MenuEntrySwitch<String>(
  607. switchType: SwitchType.scheckbox,
  608. text: translate('Always connect via relay'),
  609. getter: () async {
  610. return await _isForceAlwaysRelay(id);
  611. },
  612. setter: (bool v) async {
  613. await bind.mainSetPeerOption(
  614. id: id,
  615. key: kOptionForceAlwaysRelay,
  616. value: bool2option(kOptionForceAlwaysRelay, v));
  617. showToast(translate('Successful'));
  618. },
  619. padding: menuPadding,
  620. dismissOnClicked: true,
  621. );
  622. }
  623. @protected
  624. MenuEntryBase<String> _renameAction(String id) {
  625. return MenuEntryButton<String>(
  626. childBuilder: (TextStyle? style) => Text(
  627. translate('Rename'),
  628. style: style,
  629. ),
  630. proc: () async {
  631. String oldName = await _getAlias(id);
  632. renameDialog(
  633. oldName: oldName,
  634. onSubmit: (String newName) async {
  635. if (newName != oldName) {
  636. if (tab == PeerTabIndex.ab) {
  637. await gFFI.abModel.changeAlias(id: id, alias: newName);
  638. await bind.mainSetPeerAlias(id: id, alias: newName);
  639. } else {
  640. await bind.mainSetPeerAlias(id: id, alias: newName);
  641. showToast(translate('Successful'));
  642. _update();
  643. }
  644. }
  645. });
  646. },
  647. padding: menuPadding,
  648. dismissOnClicked: true,
  649. );
  650. }
  651. @protected
  652. MenuEntryBase<String> _removeAction(String id) {
  653. return MenuEntryButton<String>(
  654. childBuilder: (TextStyle? style) => Row(
  655. children: [
  656. Text(
  657. translate('Delete'),
  658. style: style?.copyWith(color: Colors.red),
  659. ),
  660. Expanded(
  661. child: Align(
  662. alignment: Alignment.centerRight,
  663. child: Transform.scale(
  664. scale: 0.8,
  665. child: Icon(Icons.delete_forever, color: Colors.red),
  666. ),
  667. ).marginOnly(right: 4)),
  668. ],
  669. ),
  670. proc: () {
  671. onSubmit() async {
  672. switch (tab) {
  673. case PeerTabIndex.recent:
  674. await bind.mainRemovePeer(id: id);
  675. await bind.mainLoadRecentPeers();
  676. break;
  677. case PeerTabIndex.fav:
  678. final favs = (await bind.mainGetFav()).toList();
  679. if (favs.remove(id)) {
  680. await bind.mainStoreFav(favs: favs);
  681. await bind.mainLoadFavPeers();
  682. }
  683. break;
  684. case PeerTabIndex.lan:
  685. await bind.mainRemoveDiscovered(id: id);
  686. await bind.mainLoadLanPeers();
  687. break;
  688. case PeerTabIndex.ab:
  689. await gFFI.abModel.deletePeers([id]);
  690. break;
  691. case PeerTabIndex.group:
  692. break;
  693. }
  694. if (tab != PeerTabIndex.ab) {
  695. showToast(translate('Successful'));
  696. }
  697. }
  698. deleteConfirmDialog(onSubmit,
  699. '${translate('Delete')} "${peer.alias.isEmpty ? formatID(peer.id) : peer.alias}"?');
  700. },
  701. padding: menuPadding,
  702. dismissOnClicked: true,
  703. );
  704. }
  705. @protected
  706. MenuEntryBase<String> _unrememberPasswordAction(String id) {
  707. return MenuEntryButton<String>(
  708. childBuilder: (TextStyle? style) => Text(
  709. translate('Forget Password'),
  710. style: style,
  711. ),
  712. proc: () async {
  713. bool succ = await gFFI.abModel.changePersonalHashPassword(id, '');
  714. await bind.mainForgetPassword(id: id);
  715. if (succ) {
  716. showToast(translate('Successful'));
  717. } else {
  718. if (tab.index == PeerTabIndex.ab.index) {
  719. BotToast.showText(
  720. contentColor: Colors.red, text: translate("Failed"));
  721. }
  722. }
  723. },
  724. padding: menuPadding,
  725. dismissOnClicked: true,
  726. );
  727. }
  728. @protected
  729. MenuEntryBase<String> _addFavAction(String id) {
  730. return MenuEntryButton<String>(
  731. childBuilder: (TextStyle? style) => Row(
  732. children: [
  733. Text(
  734. translate('Add to Favorites'),
  735. style: style,
  736. ),
  737. Expanded(
  738. child: Align(
  739. alignment: Alignment.centerRight,
  740. child: Transform.scale(
  741. scale: 0.8,
  742. child: Icon(Icons.star_outline),
  743. ),
  744. ).marginOnly(right: 4)),
  745. ],
  746. ),
  747. proc: () {
  748. () async {
  749. final favs = (await bind.mainGetFav()).toList();
  750. if (!favs.contains(id)) {
  751. favs.add(id);
  752. await bind.mainStoreFav(favs: favs);
  753. }
  754. showToast(translate('Successful'));
  755. }();
  756. },
  757. padding: menuPadding,
  758. dismissOnClicked: true,
  759. );
  760. }
  761. @protected
  762. MenuEntryBase<String> _rmFavAction(
  763. String id, Future<void> Function() reloadFunc) {
  764. return MenuEntryButton<String>(
  765. childBuilder: (TextStyle? style) => Row(
  766. children: [
  767. Text(
  768. translate('Remove from Favorites'),
  769. style: style,
  770. ),
  771. Expanded(
  772. child: Align(
  773. alignment: Alignment.centerRight,
  774. child: Transform.scale(
  775. scale: 0.8,
  776. child: Icon(Icons.star),
  777. ),
  778. ).marginOnly(right: 4)),
  779. ],
  780. ),
  781. proc: () {
  782. () async {
  783. final favs = (await bind.mainGetFav()).toList();
  784. if (favs.remove(id)) {
  785. await bind.mainStoreFav(favs: favs);
  786. await reloadFunc();
  787. }
  788. showToast(translate('Successful'));
  789. }();
  790. },
  791. padding: menuPadding,
  792. dismissOnClicked: true,
  793. );
  794. }
  795. @protected
  796. MenuEntryBase<String> _addToAb(Peer peer) {
  797. return MenuEntryButton<String>(
  798. childBuilder: (TextStyle? style) => Text(
  799. translate('Add to address book'),
  800. style: style,
  801. ),
  802. proc: () {
  803. () async {
  804. addPeersToAbDialog([Peer.copy(peer)]);
  805. }();
  806. },
  807. padding: menuPadding,
  808. dismissOnClicked: true,
  809. );
  810. }
  811. @protected
  812. Future<String> _getAlias(String id) async =>
  813. await bind.mainGetPeerOption(id: id, key: 'alias');
  814. @protected
  815. void _update();
  816. }
  817. class RecentPeerCard extends BasePeerCard {
  818. RecentPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
  819. : super(
  820. peer: peer,
  821. tab: PeerTabIndex.recent,
  822. menuPadding: menuPadding,
  823. key: key);
  824. @override
  825. Future<List<MenuEntryBase<String>>> _buildMenuItems(
  826. BuildContext context) async {
  827. final List<MenuEntryBase<String>> menuItems = [
  828. _connectAction(context),
  829. _transferFileAction(context),
  830. ];
  831. final List favs = (await bind.mainGetFav()).toList();
  832. if (isDesktop && peer.platform != kPeerPlatformAndroid) {
  833. menuItems.add(_tcpTunnelingAction(context));
  834. }
  835. // menuItems.add(await _openNewConnInOptAction(peer.id));
  836. if (!isWeb) {
  837. menuItems.add(await _forceAlwaysRelayAction(peer.id));
  838. }
  839. if (isWindows && peer.platform == kPeerPlatformWindows) {
  840. menuItems.add(_rdpAction(context, peer.id));
  841. }
  842. if (isWindows) {
  843. menuItems.add(_createShortCutAction(peer.id));
  844. }
  845. menuItems.add(MenuEntryDivider());
  846. if (isMobile || isDesktop || isWebDesktop) {
  847. menuItems.add(_renameAction(peer.id));
  848. }
  849. if (await bind.mainPeerHasPassword(id: peer.id)) {
  850. menuItems.add(_unrememberPasswordAction(peer.id));
  851. }
  852. if (!favs.contains(peer.id)) {
  853. menuItems.add(_addFavAction(peer.id));
  854. } else {
  855. menuItems.add(_rmFavAction(peer.id, () async {}));
  856. }
  857. if (gFFI.userModel.userName.isNotEmpty) {
  858. menuItems.add(_addToAb(peer));
  859. }
  860. menuItems.add(MenuEntryDivider());
  861. menuItems.add(_removeAction(peer.id));
  862. return menuItems;
  863. }
  864. @protected
  865. @override
  866. void _update() => bind.mainLoadRecentPeers();
  867. }
  868. class FavoritePeerCard extends BasePeerCard {
  869. FavoritePeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
  870. : super(
  871. peer: peer,
  872. tab: PeerTabIndex.fav,
  873. menuPadding: menuPadding,
  874. key: key);
  875. @override
  876. Future<List<MenuEntryBase<String>>> _buildMenuItems(
  877. BuildContext context) async {
  878. final List<MenuEntryBase<String>> menuItems = [
  879. _connectAction(context),
  880. _transferFileAction(context),
  881. ];
  882. if (isDesktop && peer.platform != kPeerPlatformAndroid) {
  883. menuItems.add(_tcpTunnelingAction(context));
  884. }
  885. // menuItems.add(await _openNewConnInOptAction(peer.id));
  886. if (!isWeb) {
  887. menuItems.add(await _forceAlwaysRelayAction(peer.id));
  888. }
  889. if (isWindows && peer.platform == kPeerPlatformWindows) {
  890. menuItems.add(_rdpAction(context, peer.id));
  891. }
  892. if (isWindows) {
  893. menuItems.add(_createShortCutAction(peer.id));
  894. }
  895. menuItems.add(MenuEntryDivider());
  896. if (isMobile || isDesktop || isWebDesktop) {
  897. menuItems.add(_renameAction(peer.id));
  898. }
  899. if (await bind.mainPeerHasPassword(id: peer.id)) {
  900. menuItems.add(_unrememberPasswordAction(peer.id));
  901. }
  902. menuItems.add(_rmFavAction(peer.id, () async {
  903. await bind.mainLoadFavPeers();
  904. }));
  905. if (gFFI.userModel.userName.isNotEmpty) {
  906. menuItems.add(_addToAb(peer));
  907. }
  908. menuItems.add(MenuEntryDivider());
  909. menuItems.add(_removeAction(peer.id));
  910. return menuItems;
  911. }
  912. @protected
  913. @override
  914. void _update() => bind.mainLoadFavPeers();
  915. }
  916. class DiscoveredPeerCard extends BasePeerCard {
  917. DiscoveredPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
  918. : super(
  919. peer: peer,
  920. tab: PeerTabIndex.lan,
  921. menuPadding: menuPadding,
  922. key: key);
  923. @override
  924. Future<List<MenuEntryBase<String>>> _buildMenuItems(
  925. BuildContext context) async {
  926. final List<MenuEntryBase<String>> menuItems = [
  927. _connectAction(context),
  928. _transferFileAction(context),
  929. ];
  930. final List favs = (await bind.mainGetFav()).toList();
  931. if (isDesktop && peer.platform != kPeerPlatformAndroid) {
  932. menuItems.add(_tcpTunnelingAction(context));
  933. }
  934. // menuItems.add(await _openNewConnInOptAction(peer.id));
  935. if (!isWeb) {
  936. menuItems.add(await _forceAlwaysRelayAction(peer.id));
  937. }
  938. if (isWindows && peer.platform == kPeerPlatformWindows) {
  939. menuItems.add(_rdpAction(context, peer.id));
  940. }
  941. menuItems.add(_wolAction(peer.id));
  942. if (isWindows) {
  943. menuItems.add(_createShortCutAction(peer.id));
  944. }
  945. if (!favs.contains(peer.id)) {
  946. menuItems.add(_addFavAction(peer.id));
  947. } else {
  948. menuItems.add(_rmFavAction(peer.id, () async {}));
  949. }
  950. if (gFFI.userModel.userName.isNotEmpty) {
  951. menuItems.add(_addToAb(peer));
  952. }
  953. menuItems.add(MenuEntryDivider());
  954. menuItems.add(_removeAction(peer.id));
  955. return menuItems;
  956. }
  957. @protected
  958. @override
  959. void _update() => bind.mainLoadLanPeers();
  960. }
  961. class AddressBookPeerCard extends BasePeerCard {
  962. AddressBookPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
  963. : super(
  964. peer: peer,
  965. tab: PeerTabIndex.ab,
  966. menuPadding: menuPadding,
  967. key: key);
  968. @override
  969. Future<List<MenuEntryBase<String>>> _buildMenuItems(
  970. BuildContext context) async {
  971. final List<MenuEntryBase<String>> menuItems = [
  972. _connectAction(context),
  973. _transferFileAction(context),
  974. ];
  975. if (isDesktop && peer.platform != kPeerPlatformAndroid) {
  976. menuItems.add(_tcpTunnelingAction(context));
  977. }
  978. // menuItems.add(await _openNewConnInOptAction(peer.id));
  979. // menuItems.add(await _forceAlwaysRelayAction(peer.id));
  980. if (isWindows && peer.platform == kPeerPlatformWindows) {
  981. menuItems.add(_rdpAction(context, peer.id));
  982. }
  983. if (isWindows) {
  984. menuItems.add(_createShortCutAction(peer.id));
  985. }
  986. if (gFFI.abModel.current.canWrite()) {
  987. menuItems.add(MenuEntryDivider());
  988. if (isMobile || isDesktop || isWebDesktop) {
  989. menuItems.add(_renameAction(peer.id));
  990. }
  991. if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) {
  992. menuItems.add(_unrememberPasswordAction(peer.id));
  993. }
  994. if (!gFFI.abModel.current.isPersonal()) {
  995. menuItems.add(_changeSharedAbPassword());
  996. }
  997. if (gFFI.abModel.currentAbTags.isNotEmpty) {
  998. menuItems.add(_editTagAction(peer.id));
  999. }
  1000. }
  1001. final addressbooks = gFFI.abModel.addressBooksCanWrite();
  1002. if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) {
  1003. addressbooks.remove(gFFI.abModel.currentName.value);
  1004. }
  1005. if (addressbooks.isNotEmpty) {
  1006. menuItems.add(_addToAb(peer));
  1007. }
  1008. menuItems.add(_existIn());
  1009. if (gFFI.abModel.current.canWrite()) {
  1010. menuItems.add(MenuEntryDivider());
  1011. menuItems.add(_removeAction(peer.id));
  1012. }
  1013. return menuItems;
  1014. }
  1015. // address book does not need to update
  1016. @protected
  1017. @override
  1018. void _update() =>
  1019. {}; //gFFI.abModel.pullAb(force: ForcePullAb.current, quiet: true);
  1020. @protected
  1021. MenuEntryBase<String> _editTagAction(String id) {
  1022. return MenuEntryButton<String>(
  1023. childBuilder: (TextStyle? style) => Text(
  1024. translate('Edit Tag'),
  1025. style: style,
  1026. ),
  1027. proc: () {
  1028. editAbTagDialog(gFFI.abModel.getPeerTags(id), (selectedTag) async {
  1029. await gFFI.abModel.changeTagForPeers([id], selectedTag);
  1030. });
  1031. },
  1032. padding: super.menuPadding,
  1033. dismissOnClicked: true,
  1034. );
  1035. }
  1036. @protected
  1037. @override
  1038. Future<String> _getAlias(String id) async =>
  1039. gFFI.abModel.find(id)?.alias ?? '';
  1040. MenuEntryBase<String> _changeSharedAbPassword() {
  1041. return MenuEntryButton<String>(
  1042. childBuilder: (TextStyle? style) => Text(
  1043. translate(
  1044. peer.password.isEmpty ? 'Set shared password' : 'Change Password'),
  1045. style: style,
  1046. ),
  1047. proc: () {
  1048. setSharedAbPasswordDialog(gFFI.abModel.currentName.value, peer);
  1049. },
  1050. padding: super.menuPadding,
  1051. dismissOnClicked: true,
  1052. );
  1053. }
  1054. MenuEntryBase<String> _existIn() {
  1055. final names = gFFI.abModel.idExistIn(peer.id);
  1056. final text = names.join(', ');
  1057. return MenuEntryButton<String>(
  1058. childBuilder: (TextStyle? style) => Text(
  1059. translate('Exist in'),
  1060. style: style,
  1061. ),
  1062. proc: () {
  1063. gFFI.dialogManager.show((setState, close, context) {
  1064. return CustomAlertDialog(
  1065. title: Text(translate('Exist in')),
  1066. content: Column(
  1067. crossAxisAlignment: CrossAxisAlignment.start,
  1068. children: [Text(text)]),
  1069. actions: [
  1070. dialogButton(
  1071. "OK",
  1072. icon: Icon(Icons.done_rounded),
  1073. onPressed: close,
  1074. ),
  1075. ],
  1076. onSubmit: close,
  1077. onCancel: close,
  1078. );
  1079. });
  1080. },
  1081. padding: super.menuPadding,
  1082. dismissOnClicked: true,
  1083. );
  1084. }
  1085. }
  1086. class MyGroupPeerCard extends BasePeerCard {
  1087. MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
  1088. : super(
  1089. peer: peer,
  1090. tab: PeerTabIndex.group,
  1091. menuPadding: menuPadding,
  1092. key: key);
  1093. @override
  1094. Future<List<MenuEntryBase<String>>> _buildMenuItems(
  1095. BuildContext context) async {
  1096. final List<MenuEntryBase<String>> menuItems = [
  1097. _connectAction(context),
  1098. _transferFileAction(context),
  1099. ];
  1100. if (isDesktop && peer.platform != kPeerPlatformAndroid) {
  1101. menuItems.add(_tcpTunnelingAction(context));
  1102. }
  1103. // menuItems.add(await _openNewConnInOptAction(peer.id));
  1104. // menuItems.add(await _forceAlwaysRelayAction(peer.id));
  1105. if (isWindows && peer.platform == kPeerPlatformWindows) {
  1106. menuItems.add(_rdpAction(context, peer.id));
  1107. }
  1108. if (isWindows) {
  1109. menuItems.add(_createShortCutAction(peer.id));
  1110. }
  1111. // menuItems.add(MenuEntryDivider());
  1112. // menuItems.add(_renameAction(peer.id));
  1113. // if (await bind.mainPeerHasPassword(id: peer.id)) {
  1114. // menuItems.add(_unrememberPasswordAction(peer.id));
  1115. // }
  1116. if (gFFI.userModel.userName.isNotEmpty) {
  1117. menuItems.add(_addToAb(peer));
  1118. }
  1119. return menuItems;
  1120. }
  1121. @protected
  1122. @override
  1123. void _update() => gFFI.groupModel.pull();
  1124. }
  1125. void _rdpDialog(String id) async {
  1126. final maxLength = bind.mainMaxEncryptLen();
  1127. final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
  1128. final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
  1129. final portController = TextEditingController(text: port);
  1130. final userController = TextEditingController(text: username);
  1131. final passwordController = TextEditingController(
  1132. text: await bind.mainGetPeerOption(id: id, key: 'rdp_password'));
  1133. RxBool secure = true.obs;
  1134. gFFI.dialogManager.show((setState, close, context) {
  1135. submit() async {
  1136. String port = portController.text.trim();
  1137. String username = userController.text;
  1138. String password = passwordController.text;
  1139. await bind.mainSetPeerOption(id: id, key: 'rdp_port', value: port);
  1140. await bind.mainSetPeerOption(
  1141. id: id, key: 'rdp_username', value: username);
  1142. await bind.mainSetPeerOption(
  1143. id: id, key: 'rdp_password', value: password);
  1144. showToast(translate('Successful'));
  1145. close();
  1146. }
  1147. return CustomAlertDialog(
  1148. title: Text(translate('RDP Settings')),
  1149. content: ConstrainedBox(
  1150. constraints: const BoxConstraints(minWidth: 500),
  1151. child: Column(
  1152. crossAxisAlignment: CrossAxisAlignment.start,
  1153. children: [
  1154. Row(
  1155. children: [
  1156. isDesktop
  1157. ? ConstrainedBox(
  1158. constraints: const BoxConstraints(minWidth: 140),
  1159. child: Text(
  1160. "${translate('Port')}:",
  1161. textAlign: TextAlign.right,
  1162. ).marginOnly(right: 10))
  1163. : SizedBox.shrink(),
  1164. Expanded(
  1165. child: TextField(
  1166. inputFormatters: [
  1167. FilteringTextInputFormatter.allow(RegExp(
  1168. r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$'))
  1169. ],
  1170. decoration: InputDecoration(
  1171. labelText: isDesktop ? null : translate('Port'),
  1172. hintText: '3389'),
  1173. controller: portController,
  1174. autofocus: true,
  1175. ).workaroundFreezeLinuxMint(),
  1176. ),
  1177. ],
  1178. ).marginOnly(bottom: isDesktop ? 8 : 0),
  1179. Obx(() => Row(
  1180. children: [
  1181. stateGlobal.isPortrait.isFalse
  1182. ? ConstrainedBox(
  1183. constraints: const BoxConstraints(minWidth: 140),
  1184. child: Text(
  1185. "${translate('Username')}:",
  1186. textAlign: TextAlign.right,
  1187. ).marginOnly(right: 10))
  1188. : SizedBox.shrink(),
  1189. Expanded(
  1190. child: TextField(
  1191. decoration: InputDecoration(
  1192. labelText:
  1193. isDesktop ? null : translate('Username')),
  1194. controller: userController,
  1195. ).workaroundFreezeLinuxMint(),
  1196. ),
  1197. ],
  1198. ).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
  1199. Obx(() => Row(
  1200. children: [
  1201. stateGlobal.isPortrait.isFalse
  1202. ? ConstrainedBox(
  1203. constraints: const BoxConstraints(minWidth: 140),
  1204. child: Text(
  1205. "${translate('Password')}:",
  1206. textAlign: TextAlign.right,
  1207. ).marginOnly(right: 10))
  1208. : SizedBox.shrink(),
  1209. Expanded(
  1210. child: Obx(() => TextField(
  1211. obscureText: secure.value,
  1212. maxLength: maxLength,
  1213. decoration: InputDecoration(
  1214. labelText:
  1215. isDesktop ? null : translate('Password'),
  1216. suffixIcon: IconButton(
  1217. onPressed: () =>
  1218. secure.value = !secure.value,
  1219. icon: Icon(secure.value
  1220. ? Icons.visibility_off
  1221. : Icons.visibility))),
  1222. controller: passwordController,
  1223. ).workaroundFreezeLinuxMint()),
  1224. ),
  1225. ],
  1226. ))
  1227. ],
  1228. ),
  1229. ),
  1230. actions: [
  1231. dialogButton("Cancel", onPressed: close, isOutline: true),
  1232. dialogButton("OK", onPressed: submit),
  1233. ],
  1234. onSubmit: submit,
  1235. onCancel: close,
  1236. );
  1237. });
  1238. }
  1239. Widget getOnline(double rightPadding, bool online) {
  1240. return Tooltip(
  1241. message: translate(online ? 'Online' : 'Offline'),
  1242. waitDuration: const Duration(seconds: 1),
  1243. child: Padding(
  1244. padding: EdgeInsets.fromLTRB(0, 4, rightPadding, 4),
  1245. child: CircleAvatar(
  1246. radius: 3, backgroundColor: online ? Colors.green : kColorWarn)));
  1247. }
  1248. Widget build_more(BuildContext context, {bool invert = false}) {
  1249. final RxBool hover = false.obs;
  1250. return InkWell(
  1251. borderRadius: BorderRadius.circular(14),
  1252. onTap: () {},
  1253. onHover: (value) => hover.value = value,
  1254. child: Obx(() => CircleAvatar(
  1255. radius: 14,
  1256. backgroundColor: hover.value
  1257. ? (invert
  1258. ? Theme.of(context).colorScheme.background
  1259. : Theme.of(context).scaffoldBackgroundColor)
  1260. : (invert
  1261. ? Theme.of(context).scaffoldBackgroundColor
  1262. : Theme.of(context).colorScheme.background),
  1263. child: Icon(Icons.more_vert,
  1264. size: 18,
  1265. color: hover.value
  1266. ? Theme.of(context).textTheme.titleLarge?.color
  1267. : Theme.of(context)
  1268. .textTheme
  1269. .titleLarge
  1270. ?.color
  1271. ?.withOpacity(0.5)))));
  1272. }
  1273. class TagPainter extends CustomPainter {
  1274. final double radius;
  1275. late final List<Color> colors;
  1276. TagPainter({required this.radius, required List<Color> colors}) {
  1277. this.colors = colors.reversed.toList();
  1278. }
  1279. @override
  1280. void paint(Canvas canvas, Size size) {
  1281. double x = 0;
  1282. double y = radius;
  1283. for (int i = 0; i < colors.length; i++) {
  1284. Paint paint = Paint();
  1285. paint.color = colors[i];
  1286. x -= radius + 1;
  1287. if (i == colors.length - 1) {
  1288. canvas.drawCircle(Offset(x, y), radius, paint);
  1289. } else {
  1290. Path path = Path();
  1291. path.addArc(Rect.fromCircle(center: Offset(x, y), radius: radius),
  1292. math.pi * 4 / 3, math.pi * 4 / 3);
  1293. path.addArc(
  1294. Rect.fromCircle(center: Offset(x - radius, y), radius: radius),
  1295. math.pi * 5 / 3,
  1296. math.pi * 2 / 3);
  1297. path.fillType = PathFillType.evenOdd;
  1298. canvas.drawPath(path, paint);
  1299. }
  1300. }
  1301. }
  1302. @override
  1303. bool shouldRepaint(covariant CustomPainter oldDelegate) {
  1304. return true;
  1305. }
  1306. }
  1307. void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
  1308. {bool isFileTransfer = false,
  1309. bool isTcpTunneling = false,
  1310. bool isRDP = false}) async {
  1311. var password = '';
  1312. bool isSharedPassword = false;
  1313. if (tab == PeerTabIndex.ab) {
  1314. // If recent peer's alias is empty, set it to ab's alias
  1315. // Because the platform is not set, it may not take effect, but it is more important not to display if the connection is not successful
  1316. if (peer.alias.isNotEmpty &&
  1317. (await bind.mainGetPeerOption(id: peer.id, key: "alias")).isEmpty) {
  1318. await bind.mainSetPeerAlias(
  1319. id: peer.id,
  1320. alias: peer.alias,
  1321. );
  1322. }
  1323. if (!gFFI.abModel.current.isPersonal()) {
  1324. if (peer.password.isNotEmpty) {
  1325. password = peer.password;
  1326. isSharedPassword = true;
  1327. }
  1328. }
  1329. }
  1330. connect(context, peer.id,
  1331. password: password,
  1332. isSharedPassword: isSharedPassword,
  1333. isFileTransfer: isFileTransfer,
  1334. isTcpTunneling: isTcpTunneling,
  1335. isRDP: isRDP);
  1336. }