mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-04 00:33:12 +00:00
* feat: add connections without sending passwords
Add Without password actions and pass the selection through desktop,
mobile and Web sessions. Omit the password when constructing LoginRequest,
preserve saved credentials, and reuse the existing approval and OS login
dialogs. Keep protocol and controlled-side authentication changes separate.
* fix: preserve login method across session windows
* fix: skip password persistence for consent connections
* refactor: unify request approval menus and naming
* chore: move request approval translation key to the end
* i18n: translate request approval menu label
* fix: align request approval menu layout and icons
* fix: avoid duplicate focus stops in request approval menus
* fix: distinguish request approval submenus on mobile
* fix: open overflowing submenus to the left
* Revert "fix: distinguish request approval submenus on mobile"
This reverts commit 7f0e50d4e4.
1609 lines
52 KiB
Dart
1609 lines
52 KiB
Dart
import 'package:bot_toast/bot_toast.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
|
import 'package:flutter_hbb/consts.dart';
|
|
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
|
import 'package:flutter_hbb/models/state_model.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../../common.dart';
|
|
import '../../common/formatter/id_formatter.dart';
|
|
import '../../models/peer_model.dart';
|
|
import '../../models/platform_model.dart';
|
|
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
|
import '../../desktop/widgets/popup_menu.dart';
|
|
import 'request_approval_menu.dart';
|
|
import 'dart:math' as math;
|
|
|
|
typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
|
|
Function(BuildContext);
|
|
|
|
enum PeerUiType { grid, tile, list }
|
|
|
|
final peerCardUiType = PeerUiType.grid.obs;
|
|
|
|
bool? hideUsernameOnCard;
|
|
|
|
class _PeerCard extends StatefulWidget {
|
|
final Peer peer;
|
|
final PeerTabIndex tab;
|
|
final Function(BuildContext, String) connect;
|
|
final PopupMenuEntryBuilder popupMenuEntryBuilder;
|
|
|
|
const _PeerCard(
|
|
{required this.peer,
|
|
required this.tab,
|
|
required this.connect,
|
|
required this.popupMenuEntryBuilder,
|
|
Key? key})
|
|
: super(key: key);
|
|
|
|
@override
|
|
_PeerCardState createState() => _PeerCardState();
|
|
}
|
|
|
|
/// State for the connection page.
|
|
class _PeerCardState extends State<_PeerCard>
|
|
with AutomaticKeepAliveClientMixin {
|
|
var _menuPos = RelativeRect.fill;
|
|
final double _cardRadius = 16;
|
|
final double _tileRadius = 5;
|
|
final double _borderWidth = 2;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
return Obx(() =>
|
|
stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape());
|
|
}
|
|
|
|
Widget gestureDetector({required Widget child}) {
|
|
final PeerTabModel peerTabModel = Provider.of(context);
|
|
final peer = super.widget.peer;
|
|
return GestureDetector(
|
|
// Web card actions have their own semantics; preserve pointer double-clicks.
|
|
excludeFromSemantics: isWebDesktop,
|
|
onDoubleTap: peerTabModel.multiSelectionMode
|
|
? null
|
|
: () => widget.connect(context, peer.id),
|
|
onTap: () {
|
|
if (peerTabModel.multiSelectionMode) {
|
|
peerTabModel.select(peer);
|
|
} else {
|
|
if (isMobile) {
|
|
widget.connect(context, peer.id);
|
|
} else {
|
|
peerTabModel.select(peer);
|
|
}
|
|
}
|
|
},
|
|
onLongPress: () => peerTabModel.select(peer),
|
|
child: child);
|
|
}
|
|
|
|
Widget _buildPortrait() {
|
|
final peer = super.widget.peer;
|
|
return Card(
|
|
margin: EdgeInsets.symmetric(horizontal: 2),
|
|
child: gestureDetector(
|
|
child: Container(
|
|
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
|
|
child: _buildPeerTile(context, peer, null)),
|
|
));
|
|
}
|
|
|
|
Widget _buildLandscape() {
|
|
final peer = super.widget.peer;
|
|
var deco = Rx<BoxDecoration?>(
|
|
BoxDecoration(
|
|
border: Border.all(color: Colors.transparent, width: _borderWidth),
|
|
borderRadius: BorderRadius.circular(
|
|
peerCardUiType.value == PeerUiType.grid ? _cardRadius : _tileRadius,
|
|
),
|
|
),
|
|
);
|
|
return MouseRegion(
|
|
onEnter: (evt) {
|
|
deco.value = BoxDecoration(
|
|
border: Border.all(
|
|
color: Theme.of(context).colorScheme.primary,
|
|
width: _borderWidth),
|
|
borderRadius: BorderRadius.circular(
|
|
peerCardUiType.value == PeerUiType.grid ? _cardRadius : _tileRadius,
|
|
),
|
|
);
|
|
},
|
|
onExit: (evt) {
|
|
deco.value = BoxDecoration(
|
|
border: Border.all(color: Colors.transparent, width: _borderWidth),
|
|
borderRadius: BorderRadius.circular(
|
|
peerCardUiType.value == PeerUiType.grid ? _cardRadius : _tileRadius,
|
|
),
|
|
);
|
|
},
|
|
child: gestureDetector(
|
|
child: Obx(() => peerCardUiType.value == PeerUiType.grid
|
|
? _buildPeerCard(context, peer, deco)
|
|
: _buildPeerTile(context, peer, deco))),
|
|
);
|
|
}
|
|
|
|
bool _showNote(Peer peer) {
|
|
return peerTabShowNote(widget.tab) && peer.note.isNotEmpty;
|
|
}
|
|
|
|
makeChild(bool isPortrait, Peer peer) {
|
|
final name = hideUsernameOnCard == true
|
|
? peer.hostname
|
|
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
|
final greyStyle = TextStyle(
|
|
fontSize: 11,
|
|
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
|
final showNote = _showNote(peer);
|
|
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.max,
|
|
children: [
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: str2color('${peer.id}${peer.platform}', 0x7f),
|
|
borderRadius: isPortrait
|
|
? BorderRadius.circular(_tileRadius)
|
|
: BorderRadius.only(
|
|
topLeft: Radius.circular(_tileRadius),
|
|
bottomLeft: Radius.circular(_tileRadius),
|
|
),
|
|
),
|
|
alignment: Alignment.center,
|
|
width: isPortrait ? 50 : 42,
|
|
height: isPortrait ? 50 : null,
|
|
child: Stack(
|
|
children: [
|
|
getPlatformImage(peer.platform, size: isPortrait ? 38 : 30)
|
|
.paddingAll(6),
|
|
if (_shouldBuildPasswordIcon(peer))
|
|
Positioned(
|
|
top: 1,
|
|
left: 1,
|
|
child: Icon(Icons.key, size: 6, color: Colors.white),
|
|
),
|
|
],
|
|
)),
|
|
Expanded(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.background,
|
|
borderRadius: BorderRadius.only(
|
|
topRight: Radius.circular(_tileRadius),
|
|
bottomRight: Radius.circular(_tileRadius),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
children: [
|
|
Row(children: [
|
|
getOnline(isPortrait ? 4 : 8, peer.online),
|
|
Expanded(
|
|
child: Text(
|
|
peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: Theme.of(context).textTheme.titleSmall,
|
|
)),
|
|
]).marginOnly(top: isPortrait ? 0 : 2),
|
|
Row(
|
|
children: [
|
|
Flexible(
|
|
child: Tooltip(
|
|
message: name,
|
|
waitDuration: const Duration(seconds: 1),
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
name,
|
|
style: isPortrait ? null : greyStyle,
|
|
textAlign: TextAlign.start,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (showNote)
|
|
Expanded(
|
|
child: Tooltip(
|
|
message: peer.note,
|
|
waitDuration: const Duration(seconds: 1),
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
peer.note,
|
|
style: isPortrait ? null : greyStyle,
|
|
textAlign: TextAlign.start,
|
|
overflow: TextOverflow.ellipsis,
|
|
).marginOnly(
|
|
left: peerCardUiType.value ==
|
|
PeerUiType.list
|
|
? 32
|
|
: 4),
|
|
),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
],
|
|
).marginOnly(top: 2),
|
|
),
|
|
isPortrait
|
|
? checkBoxOrActionMorePortrait(peer)
|
|
: checkBoxOrActionMoreLandscape(peer, isTile: true),
|
|
],
|
|
).paddingOnly(left: 10.0, top: 3.0),
|
|
),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildPeerTile(
|
|
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
|
hideUsernameOnCard ??=
|
|
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
|
final colors = _frontN(peer.tags, 25)
|
|
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
|
.toList();
|
|
return Tooltip(
|
|
message: !(isDesktop || isWebDesktop)
|
|
? ''
|
|
: peer.tags.isNotEmpty
|
|
? '${translate('Tags')}: ${peer.tags.join(', ')}'
|
|
: '',
|
|
child: Stack(children: [
|
|
Obx(
|
|
() => deco == null
|
|
? makeChild(stateGlobal.isPortrait.isTrue, peer)
|
|
: Container(
|
|
foregroundDecoration: deco.value,
|
|
child: makeChild(stateGlobal.isPortrait.isTrue, peer),
|
|
),
|
|
),
|
|
if (colors.isNotEmpty)
|
|
Obx(() => Positioned(
|
|
top: 2,
|
|
right: stateGlobal.isPortrait.isTrue ? 20 : 10,
|
|
child: CustomPaint(
|
|
painter: TagPainter(radius: 3, colors: colors),
|
|
),
|
|
))
|
|
]),
|
|
);
|
|
}
|
|
|
|
Widget _buildPeerCard(
|
|
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
|
|
hideUsernameOnCard ??=
|
|
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
|
final name = hideUsernameOnCard == true
|
|
? peer.hostname
|
|
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
|
final child = Card(
|
|
color: Colors.transparent,
|
|
elevation: 0,
|
|
margin: EdgeInsets.zero,
|
|
// to-do: memory leak here, more investigation needed.
|
|
// Continious rebuilds of `Obx()` will cause memory leak here.
|
|
// The simple demo does not have this issue.
|
|
child: Obx(
|
|
() => Container(
|
|
foregroundDecoration: deco.value,
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(_cardRadius - _borderWidth),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Expanded(
|
|
child: Container(
|
|
color: str2color('${peer.id}${peer.platform}', 0x7f),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(6),
|
|
child:
|
|
getPlatformImage(peer.platform, size: 60),
|
|
),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Tooltip(
|
|
message: name,
|
|
waitDuration: const Duration(seconds: 1),
|
|
child: Text(
|
|
name,
|
|
style: const TextStyle(
|
|
color: Colors.white70,
|
|
fontSize: 12),
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (_showNote(peer))
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Tooltip(
|
|
message: peer.note,
|
|
waitDuration: const Duration(seconds: 1),
|
|
child: Text(
|
|
peer.note,
|
|
style: const TextStyle(
|
|
color: Colors.white38,
|
|
fontSize: 10),
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
))
|
|
],
|
|
),
|
|
],
|
|
).paddingOnly(top: 4.0, left: 4.0, right: 4.0),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
color: Theme.of(context).colorScheme.background,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Row(children: [
|
|
getOnline(8, peer.online),
|
|
Expanded(
|
|
child: Text(
|
|
peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: Theme.of(context).textTheme.titleSmall,
|
|
)),
|
|
]).paddingSymmetric(vertical: 8)),
|
|
checkBoxOrActionMoreLandscape(peer, isTile: false),
|
|
],
|
|
).paddingSymmetric(horizontal: 12.0),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final colors = _frontN(peer.tags, 25)
|
|
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
|
.toList();
|
|
return Tooltip(
|
|
message: peer.tags.isNotEmpty
|
|
? '${translate('Tags')}: ${peer.tags.join(', ')}'
|
|
: '',
|
|
child: Stack(children: [
|
|
child,
|
|
if (_shouldBuildPasswordIcon(peer))
|
|
Positioned(
|
|
top: 4,
|
|
left: 12,
|
|
child: Icon(Icons.key, size: 12, color: Colors.white),
|
|
),
|
|
if (colors.isNotEmpty)
|
|
Positioned(
|
|
top: 4,
|
|
right: 12,
|
|
child: CustomPaint(
|
|
painter: TagPainter(radius: 4, colors: colors),
|
|
),
|
|
)
|
|
]),
|
|
);
|
|
}
|
|
|
|
List _frontN<T>(List list, int n) {
|
|
if (list.length <= n) {
|
|
return list;
|
|
} else {
|
|
return list.sublist(0, n);
|
|
}
|
|
}
|
|
|
|
Widget checkBoxOrActionMorePortrait(Peer peer) {
|
|
final PeerTabModel peerTabModel = Provider.of(context);
|
|
final selected = peerTabModel.isPeerSelected(peer.id);
|
|
if (peerTabModel.multiSelectionMode) {
|
|
final checkbox = Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: selected
|
|
? Icon(
|
|
Icons.check_box,
|
|
color: MyTheme.accent,
|
|
)
|
|
: Icon(Icons.check_box_outline_blank),
|
|
);
|
|
return isWebDesktop
|
|
? Semantics(
|
|
container: true,
|
|
label: formatID(peer.id),
|
|
checked: selected,
|
|
onTap: () => peerTabModel.select(peer),
|
|
child: checkbox)
|
|
: checkbox;
|
|
} else {
|
|
if (isWebDesktop) return _actionMoreWeb(peer);
|
|
return InkWell(
|
|
child: const Padding(
|
|
padding: EdgeInsets.all(12), child: Icon(Icons.more_vert)),
|
|
onTapDown: (e) {
|
|
final x = e.globalPosition.dx;
|
|
final y = e.globalPosition.dy;
|
|
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
|
},
|
|
onTap: () {
|
|
_showPeerMenu(peer.id);
|
|
});
|
|
}
|
|
}
|
|
|
|
Widget checkBoxOrActionMoreLandscape(Peer peer, {required bool isTile}) {
|
|
final PeerTabModel peerTabModel = Provider.of(context);
|
|
final selected = peerTabModel.isPeerSelected(peer.id);
|
|
if (peerTabModel.multiSelectionMode) {
|
|
final icon = selected
|
|
? Icon(
|
|
Icons.check_box,
|
|
color: MyTheme.accent,
|
|
)
|
|
: Icon(Icons.check_box_outline_blank);
|
|
final checkbox = isWebDesktop
|
|
? Semantics(
|
|
container: true,
|
|
label: formatID(peer.id),
|
|
checked: selected,
|
|
onTap: () => peerTabModel.select(peer),
|
|
child: icon)
|
|
: icon;
|
|
bool last = peerTabModel.isShiftDown && peer.id == peerTabModel.lastId;
|
|
double right = isTile ? 4 : 0;
|
|
if (last) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: MyTheme.accent, width: 1)),
|
|
child: checkbox,
|
|
).marginOnly(right: right);
|
|
} else {
|
|
return checkbox.marginOnly(right: right);
|
|
}
|
|
} else {
|
|
return isWebDesktop ? _actionMoreWeb(peer) : _actionMore(peer);
|
|
}
|
|
}
|
|
|
|
Widget _actionMore(Peer peer) => Listener(
|
|
onPointerDown: (e) {
|
|
final x = e.position.dx;
|
|
final y = e.position.dy;
|
|
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
|
},
|
|
onPointerUp: (_) => _showPeerMenu(peer.id),
|
|
child: build_more(context));
|
|
|
|
Widget _actionMoreWeb(Peer peer) => Semantics(
|
|
container: true,
|
|
button: true,
|
|
label: translate('More'),
|
|
child: Builder(
|
|
builder: (context) => build_more(context, onTap: () {
|
|
final box = context.findRenderObject() as RenderBox;
|
|
final position =
|
|
box.localToGlobal(box.size.center(Offset.zero));
|
|
_menuPos = RelativeRect.fromLTRB(
|
|
position.dx, position.dy, position.dx, position.dy);
|
|
_showPeerMenu(peer.id);
|
|
})));
|
|
|
|
bool _shouldBuildPasswordIcon(Peer peer) {
|
|
if (gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index) return false;
|
|
if (gFFI.abModel.current.isPersonal()) return false;
|
|
return peer.password.isNotEmpty;
|
|
}
|
|
|
|
/// Show the peer menu and handle user's choice.
|
|
/// User might remove the peer or send a file to the peer.
|
|
void _showPeerMenu(String id) async {
|
|
await mod_menu.showMenu(
|
|
context: context,
|
|
position: _menuPos,
|
|
items: await super.widget.popupMenuEntryBuilder(context),
|
|
elevation: 8,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
}
|
|
|
|
abstract class BasePeerCard extends StatelessWidget {
|
|
final Peer peer;
|
|
final PeerTabIndex tab;
|
|
final EdgeInsets? menuPadding;
|
|
|
|
BasePeerCard(
|
|
{required this.peer, required this.tab, this.menuPadding, Key? key})
|
|
: super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return _PeerCard(
|
|
peer: peer,
|
|
tab: tab,
|
|
connect: (BuildContext context, String id) =>
|
|
connectInPeerTab(context, peer, tab),
|
|
popupMenuEntryBuilder: _buildPopupMenuEntry,
|
|
);
|
|
}
|
|
|
|
Future<List<mod_menu.PopupMenuEntry<String>>> _buildPopupMenuEntry(
|
|
BuildContext context) async =>
|
|
(await _buildMenuItems(context))
|
|
.map((e) => e.build(
|
|
context,
|
|
const MenuConfig(
|
|
commonColor: CustomPopupMenuTheme.commonColor,
|
|
height: CustomPopupMenuTheme.height,
|
|
dividerHeight: CustomPopupMenuTheme.dividerHeight)))
|
|
.expand((i) => i)
|
|
.toList();
|
|
|
|
@protected
|
|
Future<List<MenuEntryBase<String>>> _buildMenuItems(BuildContext context);
|
|
|
|
@protected
|
|
Future<List<MenuEntryBase<String>>> _buildConnectionMenuItems(
|
|
BuildContext context) async {
|
|
final List<MenuEntryBase<String>> menuItems = [
|
|
_connectAction(context),
|
|
_transferFileAction(context),
|
|
_viewCameraAction(context),
|
|
_terminalAction(context),
|
|
];
|
|
|
|
if (peer.platform == kPeerPlatformWindows) {
|
|
menuItems.add(_terminalRunAsAdminAction(context));
|
|
}
|
|
|
|
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
|
menuItems.add(_tcpTunnelingAction(context));
|
|
}
|
|
if (isWindows && peer.platform == kPeerPlatformWindows) {
|
|
menuItems.add(_rdpAction(context, peer.id));
|
|
}
|
|
menuItems.add(MenuEntryDivider<String>());
|
|
menuItems.add(_requestApprovalAction(context));
|
|
// menuItems.add(await _openNewConnInOptAction(peer.id));
|
|
if (!isWeb) {
|
|
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
|
}
|
|
return menuItems;
|
|
}
|
|
|
|
MenuEntryButton<String> _connectCommonAction(
|
|
BuildContext context,
|
|
String title, {
|
|
bool isFileTransfer = false,
|
|
bool isViewCamera = false,
|
|
bool isTcpTunneling = false,
|
|
bool isRDP = false,
|
|
bool isTerminal = false,
|
|
bool isTerminalRunAsAdmin = false,
|
|
String loginMethod = '',
|
|
}) {
|
|
return MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Text(title, style: style),
|
|
proc: () {
|
|
if (isTerminalRunAsAdmin) {
|
|
setEnvTerminalAdmin();
|
|
}
|
|
connectInPeerTab(
|
|
context,
|
|
peer,
|
|
tab,
|
|
isFileTransfer: isFileTransfer,
|
|
isViewCamera: isViewCamera,
|
|
isTcpTunneling: isTcpTunneling,
|
|
isRDP: isRDP,
|
|
isTerminal: isTerminal || isTerminalRunAsAdmin,
|
|
loginMethod: loginMethod,
|
|
);
|
|
},
|
|
padding: menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
}
|
|
|
|
MenuEntryBase<String> _requestApprovalAction(BuildContext context) {
|
|
MenuEntryButton<String> action(String title,
|
|
{bool isFileTransfer = false,
|
|
bool isViewCamera = false,
|
|
bool isTcpTunneling = false,
|
|
bool isRDP = false,
|
|
bool isTerminal = false,
|
|
bool isTerminalRunAsAdmin = false}) =>
|
|
_connectCommonAction(
|
|
context,
|
|
isTerminal || isTerminalRunAsAdmin
|
|
? '${translate(title)} (beta)'
|
|
: translate(title),
|
|
isFileTransfer: isFileTransfer,
|
|
isViewCamera: isViewCamera,
|
|
isTcpTunneling: isTcpTunneling,
|
|
isRDP: isRDP,
|
|
isTerminal: isTerminal,
|
|
isTerminalRunAsAdmin: isTerminalRunAsAdmin,
|
|
loginMethod: 'request-approval',
|
|
);
|
|
|
|
return RequestApprovalMenu(
|
|
title: translate('Request approval'),
|
|
padding: menuPadding,
|
|
entries: [
|
|
action('Connect'),
|
|
action('Transfer file', isFileTransfer: true),
|
|
action('View camera', isViewCamera: true),
|
|
action('Terminal', isTerminal: true),
|
|
if (peer.platform == kPeerPlatformWindows)
|
|
action('Terminal (Run as administrator)', isTerminalRunAsAdmin: true),
|
|
if (isDesktop && peer.platform != kPeerPlatformAndroid)
|
|
action('TCP tunneling', isTcpTunneling: true),
|
|
if (isWindows && peer.platform == kPeerPlatformWindows)
|
|
action('RDP', isRDP: true),
|
|
],
|
|
);
|
|
}
|
|
|
|
@protected
|
|
MenuEntryBase<String> _connectAction(BuildContext context) {
|
|
return _connectCommonAction(
|
|
context,
|
|
peer.alias.isEmpty
|
|
? translate('Connect')
|
|
: '${translate('Connect')} ${peer.id}',
|
|
);
|
|
}
|
|
|
|
@protected
|
|
MenuEntryBase<String> _transferFileAction(BuildContext context) {
|
|
return _connectCommonAction(
|
|
context,
|
|
translate('Transfer file'),
|
|
isFileTransfer: true,
|
|
);
|
|
}
|
|
|
|
@protected
|
|
MenuEntryBase<String> _viewCameraAction(BuildContext context) {
|
|
return _connectCommonAction(
|
|
context,
|
|
translate('View camera'),
|
|
isViewCamera: true,
|
|
);
|
|
}
|
|
|
|
@protected
|
|
MenuEntryBase<String> _terminalAction(BuildContext context) {
|
|
return _connectCommonAction(
|
|
context,
|
|
'${translate('Terminal')} (beta)',
|
|
isTerminal: true,
|
|
);
|
|
}
|
|
|
|
@protected
|
|
MenuEntryBase<String> _terminalRunAsAdminAction(BuildContext context) {
|
|
return _connectCommonAction(
|
|
context,
|
|
'${translate('Terminal (Run as administrator)')} (beta)',
|
|
isTerminalRunAsAdmin: true,
|
|
);
|
|
}
|
|
|
|
@protected
|
|
MenuEntryBase<String> _tcpTunnelingAction(BuildContext context) {
|
|
return _connectCommonAction(
|
|
context,
|
|
translate('TCP tunneling'),
|
|
isTcpTunneling: true,
|
|
);
|
|
}
|
|
|
|
@protected
|
|
MenuEntryBase<String> _rdpAction(BuildContext context, String id) {
|
|
return MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Container(
|
|
alignment: AlignmentDirectional.center,
|
|
height: CustomPopupMenuTheme.height,
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
translate('RDP'),
|
|
style: style,
|
|
),
|
|
Expanded(
|
|
child: Align(
|
|
alignment: Alignment.centerRight,
|
|
child: Transform.scale(
|
|
scale: 0.8,
|
|
child: IconButton(
|
|
icon: const Icon(Icons.edit),
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(
|
|
minWidth: kMinInteractiveDimension - 8,
|
|
minHeight: kMinInteractiveDimension,
|
|
),
|
|
onPressed: () {
|
|
if (Navigator.canPop(context)) {
|
|
Navigator.pop(context);
|
|
}
|
|
_rdpDialog(id);
|
|
},
|
|
)),
|
|
))
|
|
],
|
|
)),
|
|
proc: () {
|
|
connectInPeerTab(context, peer, tab, isRDP: true);
|
|
},
|
|
padding: menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
}
|
|
|
|
@protected
|
|
MenuEntryBase<String> _wolAction(String id) {
|
|
return MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Text(
|
|
translate('WOL'),
|
|
style: style,
|
|
),
|
|
proc: () {
|
|
bind.mainWol(id: id);
|
|
},
|
|
padding: menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
}
|
|
|
|
/// Only available on Windows.
|
|
@protected
|
|
MenuEntryBase<String> _createShortCutAction(String id) {
|
|
return MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Text(
|
|
translate('Create desktop shortcut'),
|
|
style: style,
|
|
),
|
|
proc: () {
|
|
bind.mainCreateShortcut(id: id);
|
|
showToast(translate('Successful'));
|
|
},
|
|
padding: menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
}
|
|
|
|
Future<MenuEntryBase<String>> _openNewConnInAction(
|
|
String id, String label, String key) async {
|
|
return MenuEntrySwitch<String>(
|
|
switchType: SwitchType.scheckbox,
|
|
text: translate(label),
|
|
getter: () async => mainGetPeerBoolOptionSync(id, key),
|
|
setter: (bool v) async {
|
|
await bind.mainSetPeerOption(
|
|
id: id, key: key, value: bool2option(key, v));
|
|
showToast(translate('Successful'));
|
|
},
|
|
padding: menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
}
|
|
|
|
_openInTabsAction(String id) async =>
|
|
await _openNewConnInAction(id, 'Open in New Tab', kOptionOpenInTabs);
|
|
|
|
_openInWindowsAction(String id) async => await _openNewConnInAction(
|
|
id, 'Open in new window', kOptionOpenInWindows);
|
|
|
|
// ignore: unused_element
|
|
_openNewConnInOptAction(String id) async =>
|
|
mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
|
|
? await _openInWindowsAction(id)
|
|
: await _openInTabsAction(id);
|
|
|
|
@protected
|
|
Future<bool> _isForceAlwaysRelay(String id) async {
|
|
return option2bool(kOptionForceAlwaysRelay,
|
|
(await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay)));
|
|
}
|
|
|
|
@protected
|
|
Future<MenuEntryBase<String>> _forceAlwaysRelayAction(String id) async {
|
|
return MenuEntrySwitch<String>(
|
|
switchType: SwitchType.scheckbox,
|
|
text: translate('Always connect via relay'),
|
|
getter: () async {
|
|
return await _isForceAlwaysRelay(id);
|
|
},
|
|
setter: (bool v) async {
|
|
await bind.mainSetPeerOption(
|
|
id: id,
|
|
key: kOptionForceAlwaysRelay,
|
|
value: bool2option(kOptionForceAlwaysRelay, v));
|
|
showToast(translate('Successful'));
|
|
},
|
|
padding: menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
}
|
|
|
|
@protected
|
|
MenuEntryBase<String> _renameAction(String id) {
|
|
return MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Text(
|
|
translate('Rename'),
|
|
style: style,
|
|
),
|
|
proc: () async {
|
|
String oldName = await _getAlias(id);
|
|
renameDialog(
|
|
oldName: oldName,
|
|
onSubmit: (String newName) async {
|
|
if (newName != oldName) {
|
|
if (tab == PeerTabIndex.ab) {
|
|
await gFFI.abModel.changeAlias(id: id, alias: newName);
|
|
await bind.mainSetPeerAlias(id: id, alias: newName);
|
|
} else {
|
|
await bind.mainSetPeerAlias(id: id, alias: newName);
|
|
showToast(translate('Successful'));
|
|
_update();
|
|
}
|
|
}
|
|
});
|
|
},
|
|
padding: menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
}
|
|
|
|
@protected
|
|
MenuEntryBase<String> _removeAction(String id) {
|
|
return MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Row(
|
|
children: [
|
|
Text(
|
|
translate('Delete'),
|
|
style: style?.copyWith(color: Colors.red),
|
|
),
|
|
Expanded(
|
|
child: Align(
|
|
alignment: Alignment.centerRight,
|
|
child: Transform.scale(
|
|
scale: 0.8,
|
|
child: Icon(Icons.delete_forever, color: Colors.red),
|
|
),
|
|
).marginOnly(right: 4)),
|
|
],
|
|
),
|
|
proc: () {
|
|
onSubmit() async {
|
|
switch (tab) {
|
|
case PeerTabIndex.recent:
|
|
await bind.mainRemovePeer(id: id);
|
|
bind.mainLoadRecentPeers();
|
|
break;
|
|
case PeerTabIndex.fav:
|
|
final favs = (await bind.mainGetFav()).toList();
|
|
if (favs.remove(id)) {
|
|
await bind.mainStoreFav(favs: favs);
|
|
bind.mainLoadFavPeers();
|
|
}
|
|
break;
|
|
case PeerTabIndex.lan:
|
|
await bind.mainRemoveDiscovered(id: id);
|
|
bind.mainLoadLanPeers();
|
|
break;
|
|
case PeerTabIndex.ab:
|
|
await gFFI.abModel.deletePeers([id]);
|
|
break;
|
|
case PeerTabIndex.group:
|
|
break;
|
|
}
|
|
if (tab != PeerTabIndex.ab) {
|
|
showToast(translate('Successful'));
|
|
}
|
|
}
|
|
|
|
deleteConfirmDialog(onSubmit,
|
|
'${translate('Delete')} "${peer.alias.isEmpty ? formatID(peer.id) : peer.alias}"?');
|
|
},
|
|
padding: menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
}
|
|
|
|
@protected
|
|
MenuEntryBase<String> _unrememberPasswordAction(String id) {
|
|
return MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Text(
|
|
translate('Forget Password'),
|
|
style: style,
|
|
),
|
|
proc: () async {
|
|
bool succ = await gFFI.abModel.changePersonalHashPassword(id, '');
|
|
await bind.mainForgetPassword(id: id);
|
|
if (succ) {
|
|
showToast(translate('Successful'));
|
|
} else {
|
|
if (tab.index == PeerTabIndex.ab.index) {
|
|
BotToast.showText(
|
|
contentColor: Colors.red, text: translate("Failed"));
|
|
}
|
|
}
|
|
},
|
|
padding: menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
}
|
|
|
|
@protected
|
|
MenuEntryBase<String> _addFavAction(String id) {
|
|
return MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Row(
|
|
children: [
|
|
Text(
|
|
translate('Add to Favorites'),
|
|
style: style,
|
|
),
|
|
Expanded(
|
|
child: Align(
|
|
alignment: Alignment.centerRight,
|
|
child: Transform.scale(
|
|
scale: 0.8,
|
|
child: Icon(Icons.star_outline),
|
|
),
|
|
).marginOnly(right: 4)),
|
|
],
|
|
),
|
|
proc: () {
|
|
() async {
|
|
final favs = (await bind.mainGetFav()).toList();
|
|
if (!favs.contains(id)) {
|
|
favs.add(id);
|
|
await bind.mainStoreFav(favs: favs);
|
|
}
|
|
showToast(translate('Successful'));
|
|
}();
|
|
},
|
|
padding: menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
}
|
|
|
|
@protected
|
|
MenuEntryBase<String> _rmFavAction(
|
|
String id, Future<void> Function() reloadFunc) {
|
|
return MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Row(
|
|
children: [
|
|
Text(
|
|
translate('Remove from Favorites'),
|
|
style: style,
|
|
),
|
|
Expanded(
|
|
child: Align(
|
|
alignment: Alignment.centerRight,
|
|
child: Transform.scale(
|
|
scale: 0.8,
|
|
child: Icon(Icons.star),
|
|
),
|
|
).marginOnly(right: 4)),
|
|
],
|
|
),
|
|
proc: () {
|
|
() async {
|
|
final favs = (await bind.mainGetFav()).toList();
|
|
if (favs.remove(id)) {
|
|
await bind.mainStoreFav(favs: favs);
|
|
await reloadFunc();
|
|
}
|
|
showToast(translate('Successful'));
|
|
}();
|
|
},
|
|
padding: menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
}
|
|
|
|
@protected
|
|
MenuEntryBase<String> _addToAb(Peer peer) {
|
|
return MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Text(
|
|
translate('Add to address book'),
|
|
style: style,
|
|
),
|
|
proc: () {
|
|
() async {
|
|
addPeersToAbDialog([Peer.copy(peer)]);
|
|
}();
|
|
},
|
|
padding: menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
}
|
|
|
|
@protected
|
|
Future<String> _getAlias(String id) async =>
|
|
await bind.mainGetPeerOption(id: id, key: 'alias');
|
|
|
|
@protected
|
|
void _update();
|
|
}
|
|
|
|
class RecentPeerCard extends BasePeerCard {
|
|
RecentPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
|
|
: super(
|
|
peer: peer,
|
|
tab: PeerTabIndex.recent,
|
|
menuPadding: menuPadding,
|
|
key: key);
|
|
|
|
@override
|
|
Future<List<MenuEntryBase<String>>> _buildMenuItems(
|
|
BuildContext context) async {
|
|
final List favs = (await bind.mainGetFav()).toList();
|
|
|
|
final menuItems = await _buildConnectionMenuItems(context);
|
|
menuItems.add(MenuEntryDivider());
|
|
if (isWindows) {
|
|
menuItems.add(_createShortCutAction(peer.id));
|
|
}
|
|
if (isMobile || isDesktop || isWebDesktop) {
|
|
menuItems.add(_renameAction(peer.id));
|
|
}
|
|
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
|
menuItems.add(_unrememberPasswordAction(peer.id));
|
|
}
|
|
|
|
if (!favs.contains(peer.id)) {
|
|
menuItems.add(_addFavAction(peer.id));
|
|
} else {
|
|
menuItems.add(_rmFavAction(peer.id, () async {}));
|
|
}
|
|
|
|
if (gFFI.userModel.userName.isNotEmpty) {
|
|
menuItems.add(_addToAb(peer));
|
|
}
|
|
|
|
menuItems.add(MenuEntryDivider());
|
|
menuItems.add(_removeAction(peer.id));
|
|
return menuItems;
|
|
}
|
|
|
|
@protected
|
|
@override
|
|
void _update() => bind.mainLoadRecentPeers();
|
|
}
|
|
|
|
class FavoritePeerCard extends BasePeerCard {
|
|
FavoritePeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
|
|
: super(
|
|
peer: peer,
|
|
tab: PeerTabIndex.fav,
|
|
menuPadding: menuPadding,
|
|
key: key);
|
|
|
|
@override
|
|
Future<List<MenuEntryBase<String>>> _buildMenuItems(
|
|
BuildContext context) async {
|
|
final menuItems = await _buildConnectionMenuItems(context);
|
|
menuItems.add(MenuEntryDivider());
|
|
if (isWindows) {
|
|
menuItems.add(_createShortCutAction(peer.id));
|
|
}
|
|
if (isMobile || isDesktop || isWebDesktop) {
|
|
menuItems.add(_renameAction(peer.id));
|
|
}
|
|
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
|
menuItems.add(_unrememberPasswordAction(peer.id));
|
|
}
|
|
menuItems.add(_rmFavAction(peer.id, () async {
|
|
await bind.mainLoadFavPeers();
|
|
}));
|
|
|
|
if (gFFI.userModel.userName.isNotEmpty) {
|
|
menuItems.add(_addToAb(peer));
|
|
}
|
|
|
|
menuItems.add(MenuEntryDivider());
|
|
menuItems.add(_removeAction(peer.id));
|
|
return menuItems;
|
|
}
|
|
|
|
@protected
|
|
@override
|
|
void _update() => bind.mainLoadFavPeers();
|
|
}
|
|
|
|
class DiscoveredPeerCard extends BasePeerCard {
|
|
DiscoveredPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
|
|
: super(
|
|
peer: peer,
|
|
tab: PeerTabIndex.lan,
|
|
menuPadding: menuPadding,
|
|
key: key);
|
|
|
|
@override
|
|
Future<List<MenuEntryBase<String>>> _buildMenuItems(
|
|
BuildContext context) async {
|
|
final List favs = (await bind.mainGetFav()).toList();
|
|
|
|
final menuItems = await _buildConnectionMenuItems(context);
|
|
menuItems.add(MenuEntryDivider());
|
|
menuItems.add(_wolAction(peer.id));
|
|
if (isWindows) {
|
|
menuItems.add(_createShortCutAction(peer.id));
|
|
}
|
|
|
|
if (!favs.contains(peer.id)) {
|
|
menuItems.add(_addFavAction(peer.id));
|
|
} else {
|
|
menuItems.add(_rmFavAction(peer.id, () async {}));
|
|
}
|
|
|
|
if (gFFI.userModel.userName.isNotEmpty) {
|
|
menuItems.add(_addToAb(peer));
|
|
}
|
|
|
|
menuItems.add(MenuEntryDivider());
|
|
menuItems.add(_removeAction(peer.id));
|
|
return menuItems;
|
|
}
|
|
|
|
@protected
|
|
@override
|
|
void _update() => bind.mainLoadLanPeers();
|
|
}
|
|
|
|
class AddressBookPeerCard extends BasePeerCard {
|
|
AddressBookPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
|
|
: super(
|
|
peer: peer,
|
|
tab: PeerTabIndex.ab,
|
|
menuPadding: menuPadding,
|
|
key: key);
|
|
|
|
@override
|
|
Future<List<MenuEntryBase<String>>> _buildMenuItems(
|
|
BuildContext context) async {
|
|
final menuItems = await _buildConnectionMenuItems(context);
|
|
menuItems.add(MenuEntryDivider());
|
|
if (isWindows) {
|
|
menuItems.add(_createShortCutAction(peer.id));
|
|
}
|
|
if (gFFI.abModel.current.canWrite()) {
|
|
if (isMobile || isDesktop || isWebDesktop) {
|
|
menuItems.add(_renameAction(peer.id));
|
|
}
|
|
if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) {
|
|
menuItems.add(_unrememberPasswordAction(peer.id));
|
|
}
|
|
if (!gFFI.abModel.current.isPersonal()) {
|
|
menuItems.add(_changeSharedAbPassword());
|
|
}
|
|
if (gFFI.abModel.currentAbTags.isNotEmpty) {
|
|
menuItems.add(_editTagAction(peer.id));
|
|
}
|
|
menuItems.add(_editNoteAction(peer.id));
|
|
}
|
|
final addressbooks = gFFI.abModel.addressBooksCanWrite();
|
|
if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) {
|
|
addressbooks.remove(gFFI.abModel.currentName.value);
|
|
}
|
|
if (addressbooks.isNotEmpty) {
|
|
menuItems.add(_addToAb(peer));
|
|
}
|
|
menuItems.add(_existIn());
|
|
if (gFFI.abModel.current.canWrite()) {
|
|
menuItems.add(MenuEntryDivider());
|
|
menuItems.add(_removeAction(peer.id));
|
|
}
|
|
return menuItems;
|
|
}
|
|
|
|
// address book does not need to update
|
|
@protected
|
|
@override
|
|
void _update() =>
|
|
{}; //gFFI.abModel.pullAb(force: ForcePullAb.current, quiet: true);
|
|
|
|
@protected
|
|
MenuEntryBase<String> _editTagAction(String id) {
|
|
return MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Text(
|
|
translate('Edit Tag'),
|
|
style: style,
|
|
),
|
|
proc: () {
|
|
editAbTagDialog(gFFI.abModel.getPeerTags(id), (selectedTag) async {
|
|
await gFFI.abModel.changeTagForPeers([id], selectedTag);
|
|
});
|
|
},
|
|
padding: super.menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
}
|
|
|
|
@protected
|
|
MenuEntryBase<String> _editNoteAction(String id) {
|
|
return MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Text(
|
|
translate('Edit note'),
|
|
style: style,
|
|
),
|
|
proc: () {
|
|
editAbPeerNoteDialog(id);
|
|
},
|
|
padding: super.menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
}
|
|
|
|
@protected
|
|
@override
|
|
Future<String> _getAlias(String id) async =>
|
|
gFFI.abModel.find(id)?.alias ?? '';
|
|
|
|
MenuEntryBase<String> _changeSharedAbPassword() {
|
|
return MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Text(
|
|
translate(
|
|
peer.password.isEmpty ? 'Set shared password' : 'Change Password'),
|
|
style: style,
|
|
),
|
|
proc: () {
|
|
setSharedAbPasswordDialog(gFFI.abModel.currentName.value, peer);
|
|
},
|
|
padding: super.menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
}
|
|
|
|
MenuEntryBase<String> _existIn() {
|
|
final names = gFFI.abModel.idExistIn(peer.id);
|
|
final text = names.join(', ');
|
|
return MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => Text(
|
|
translate('Exist in'),
|
|
style: style,
|
|
),
|
|
proc: () {
|
|
gFFI.dialogManager.show((setState, close, context) {
|
|
return CustomAlertDialog(
|
|
title: Text(translate('Exist in')),
|
|
content: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [Text(text)]),
|
|
actions: [
|
|
dialogButton(
|
|
"OK",
|
|
icon: Icon(Icons.done_rounded),
|
|
onPressed: close,
|
|
),
|
|
],
|
|
onSubmit: close,
|
|
onCancel: close,
|
|
);
|
|
});
|
|
},
|
|
padding: super.menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
}
|
|
}
|
|
|
|
class MyGroupPeerCard extends BasePeerCard {
|
|
MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
|
|
: super(
|
|
peer: peer,
|
|
tab: PeerTabIndex.group,
|
|
menuPadding: menuPadding,
|
|
key: key);
|
|
|
|
@override
|
|
Future<List<MenuEntryBase<String>>> _buildMenuItems(
|
|
BuildContext context) async {
|
|
final menuItems = await _buildConnectionMenuItems(context);
|
|
if (isWindows || gFFI.userModel.userName.isNotEmpty) {
|
|
menuItems.add(MenuEntryDivider());
|
|
}
|
|
if (isWindows) {
|
|
menuItems.add(_createShortCutAction(peer.id));
|
|
}
|
|
// menuItems.add(MenuEntryDivider());
|
|
// menuItems.add(_renameAction(peer.id));
|
|
// if (await bind.mainPeerHasPassword(id: peer.id)) {
|
|
// menuItems.add(_unrememberPasswordAction(peer.id));
|
|
// }
|
|
if (gFFI.userModel.userName.isNotEmpty) {
|
|
menuItems.add(_addToAb(peer));
|
|
}
|
|
return menuItems;
|
|
}
|
|
|
|
@protected
|
|
@override
|
|
void _update() => gFFI.groupModel.pull();
|
|
}
|
|
|
|
void _rdpDialog(String id) async {
|
|
final maxLength = bind.mainMaxEncryptLen();
|
|
final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
|
|
final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
|
|
final portController = TextEditingController(text: port);
|
|
final userController = TextEditingController(text: username);
|
|
final passwordController = TextEditingController(
|
|
text: await bind.mainGetPeerOption(id: id, key: 'rdp_password'));
|
|
RxBool secure = true.obs;
|
|
|
|
gFFI.dialogManager.show((setState, close, context) {
|
|
submit() async {
|
|
String port = portController.text.trim();
|
|
String username = userController.text;
|
|
String password = passwordController.text;
|
|
await bind.mainSetPeerOption(id: id, key: 'rdp_port', value: port);
|
|
await bind.mainSetPeerOption(
|
|
id: id, key: 'rdp_username', value: username);
|
|
await bind.mainSetPeerOption(
|
|
id: id, key: 'rdp_password', value: password);
|
|
showToast(translate('Successful'));
|
|
close();
|
|
}
|
|
|
|
return CustomAlertDialog(
|
|
title: Text(translate('RDP Settings')),
|
|
content: ConstrainedBox(
|
|
constraints: const BoxConstraints(minWidth: 500),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
isDesktop
|
|
? ConstrainedBox(
|
|
constraints: const BoxConstraints(minWidth: 140),
|
|
child: Text(
|
|
"${translate('Port')}:",
|
|
textAlign: TextAlign.right,
|
|
).marginOnly(right: 10))
|
|
: SizedBox.shrink(),
|
|
Expanded(
|
|
child: TextField(
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.allow(RegExp(
|
|
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])$'))
|
|
],
|
|
decoration: InputDecoration(
|
|
labelText: isDesktop ? null : translate('Port'),
|
|
hintText: '3389'),
|
|
controller: portController,
|
|
autofocus: true,
|
|
).workaroundFreezeLinuxMint(),
|
|
),
|
|
],
|
|
).marginOnly(bottom: isDesktop ? 8 : 0),
|
|
Obx(() => Row(
|
|
children: [
|
|
stateGlobal.isPortrait.isFalse
|
|
? ConstrainedBox(
|
|
constraints: const BoxConstraints(minWidth: 140),
|
|
child: Text(
|
|
"${translate('Username')}:",
|
|
textAlign: TextAlign.right,
|
|
).marginOnly(right: 10))
|
|
: SizedBox.shrink(),
|
|
Expanded(
|
|
child: TextField(
|
|
decoration: InputDecoration(
|
|
labelText:
|
|
isDesktop ? null : translate('Username')),
|
|
controller: userController,
|
|
).workaroundFreezeLinuxMint(),
|
|
),
|
|
],
|
|
).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
|
|
Obx(() => Row(
|
|
children: [
|
|
stateGlobal.isPortrait.isFalse
|
|
? ConstrainedBox(
|
|
constraints: const BoxConstraints(minWidth: 140),
|
|
child: Text(
|
|
"${translate('Password')}:",
|
|
textAlign: TextAlign.right,
|
|
).marginOnly(right: 10))
|
|
: SizedBox.shrink(),
|
|
Expanded(
|
|
child: Obx(() => TextField(
|
|
obscureText: secure.value,
|
|
maxLength: maxLength,
|
|
decoration: InputDecoration(
|
|
labelText:
|
|
isDesktop ? null : translate('Password'),
|
|
suffixIcon: IconButton(
|
|
onPressed: () =>
|
|
secure.value = !secure.value,
|
|
icon: Icon(secure.value
|
|
? Icons.visibility_off
|
|
: Icons.visibility))),
|
|
controller: passwordController,
|
|
).workaroundFreezeLinuxMint()),
|
|
),
|
|
],
|
|
))
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
|
dialogButton("OK", onPressed: submit),
|
|
],
|
|
onSubmit: submit,
|
|
onCancel: close,
|
|
);
|
|
});
|
|
}
|
|
|
|
Widget getOnline(double rightPadding, bool online) {
|
|
return Tooltip(
|
|
message: translate(online ? 'Online' : 'Offline'),
|
|
waitDuration: const Duration(seconds: 1),
|
|
child: Padding(
|
|
padding: EdgeInsets.fromLTRB(0, 4, rightPadding, 4),
|
|
child: CircleAvatar(
|
|
radius: 3, backgroundColor: online ? Colors.green : kColorWarn)));
|
|
}
|
|
|
|
Widget build_more(BuildContext context,
|
|
{bool invert = false, VoidCallback? onTap}) {
|
|
final RxBool hover = false.obs;
|
|
final button = InkWell(
|
|
borderRadius: BorderRadius.circular(14),
|
|
onTap: onTap ?? () {},
|
|
onHover: (value) => hover.value = value,
|
|
child: Obx(() => CircleAvatar(
|
|
radius: 14,
|
|
backgroundColor: hover.value
|
|
? (invert
|
|
? Theme.of(context).colorScheme.background
|
|
: Theme.of(context).scaffoldBackgroundColor)
|
|
: (invert
|
|
? Theme.of(context).scaffoldBackgroundColor
|
|
: Theme.of(context).colorScheme.background),
|
|
child: Icon(Icons.more_vert,
|
|
size: 18,
|
|
color: hover.value
|
|
? Theme.of(context).textTheme.titleLarge?.color
|
|
: Theme.of(context)
|
|
.textTheme
|
|
.titleLarge
|
|
?.color
|
|
?.withOpacity(0.5)))));
|
|
if (onTap == null) return button;
|
|
// Serial taps preserve cancellation without waiting for the card's double tap.
|
|
return RawGestureDetector(
|
|
excludeFromSemantics: true,
|
|
gestures: {
|
|
SerialTapGestureRecognizer:
|
|
GestureRecognizerFactoryWithHandlers<SerialTapGestureRecognizer>(
|
|
() => SerialTapGestureRecognizer(
|
|
allowedButtonsFilter: (buttons) =>
|
|
buttons == kPrimaryButton),
|
|
(recognizer) => recognizer.onSerialTapUp = (_) => onTap()),
|
|
},
|
|
child: button);
|
|
}
|
|
|
|
class TagPainter extends CustomPainter {
|
|
final double radius;
|
|
late final List<Color> colors;
|
|
|
|
TagPainter({required this.radius, required List<Color> colors}) {
|
|
this.colors = colors.reversed.toList();
|
|
}
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
double x = 0;
|
|
double y = radius;
|
|
for (int i = 0; i < colors.length; i++) {
|
|
Paint paint = Paint();
|
|
paint.color = colors[i];
|
|
x -= radius + 1;
|
|
if (i == colors.length - 1) {
|
|
canvas.drawCircle(Offset(x, y), radius, paint);
|
|
} else {
|
|
Path path = Path();
|
|
path.addArc(Rect.fromCircle(center: Offset(x, y), radius: radius),
|
|
math.pi * 4 / 3, math.pi * 4 / 3);
|
|
path.addArc(
|
|
Rect.fromCircle(center: Offset(x - radius, y), radius: radius),
|
|
math.pi * 5 / 3,
|
|
math.pi * 2 / 3);
|
|
path.fillType = PathFillType.evenOdd;
|
|
canvas.drawPath(path, paint);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
|
|
{bool isFileTransfer = false,
|
|
bool isViewCamera = false,
|
|
bool isTcpTunneling = false,
|
|
bool isRDP = false,
|
|
bool isTerminal = false,
|
|
String loginMethod = ''}) async {
|
|
var password = '';
|
|
bool isSharedPassword = false;
|
|
if (tab == PeerTabIndex.ab) {
|
|
// If recent peer's alias is empty, set it to ab's alias
|
|
// 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
|
|
if (peer.alias.isNotEmpty &&
|
|
(await bind.mainGetPeerOption(id: peer.id, key: "alias")).isEmpty) {
|
|
await bind.mainSetPeerAlias(
|
|
id: peer.id,
|
|
alias: peer.alias,
|
|
);
|
|
}
|
|
if (!gFFI.abModel.current.isPersonal()) {
|
|
if (peer.password.isNotEmpty) {
|
|
password = peer.password;
|
|
isSharedPassword = true;
|
|
}
|
|
if (password.isEmpty) {
|
|
final abPassword = gFFI.abModel.getdefaultSharedPassword();
|
|
if (abPassword != null) {
|
|
password = abPassword;
|
|
isSharedPassword = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
connect(context, peer.id,
|
|
loginMethod: loginMethod,
|
|
password: password,
|
|
isSharedPassword: isSharedPassword,
|
|
isFileTransfer: isFileTransfer,
|
|
isTerminal: isTerminal,
|
|
isViewCamera: isViewCamera,
|
|
isTcpTunneling: isTcpTunneling,
|
|
isRDP: isRDP);
|
|
}
|