mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-04 08:34:12 +00:00
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.
1646 lines
53 KiB
Dart
1646 lines
53 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 'login_method_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);
|
|
|
|
MenuEntryBase<String> _connectCommonAction(
|
|
BuildContext context,
|
|
String title, {
|
|
bool isFileTransfer = false,
|
|
bool isViewCamera = false,
|
|
bool isTcpTunneling = false,
|
|
bool isRDP = false,
|
|
bool isTerminal = false,
|
|
bool isTerminalRunAsAdmin = false,
|
|
MenuEntryButton<String>? defaultEntry,
|
|
}) {
|
|
action(String text, String loginMethod) => MenuEntryButton<String>(
|
|
childBuilder: (TextStyle? style) => loginMethod == 'consent'
|
|
? Tooltip(
|
|
message: translate(
|
|
'Skip saved passwords and ask the remote user to accept the connection.'),
|
|
child: Text(text, style: style),
|
|
)
|
|
: Text(text, 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,
|
|
);
|
|
return LoginMethodMenu(
|
|
defaultEntry: defaultEntry ?? action(title, ''),
|
|
methods: [
|
|
action(translate('Without password'), 'consent'),
|
|
],
|
|
);
|
|
}
|
|
|
|
@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) {
|
|
final action = 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,
|
|
onPressed: () {
|
|
if (Navigator.canPop(context)) {
|
|
Navigator.pop(context);
|
|
}
|
|
_rdpDialog(id);
|
|
},
|
|
)),
|
|
))
|
|
],
|
|
)),
|
|
proc: () {
|
|
connectInPeerTab(context, peer, tab, isRDP: true);
|
|
},
|
|
padding: menuPadding,
|
|
dismissOnClicked: true,
|
|
);
|
|
return _connectCommonAction(context, translate('RDP'),
|
|
isRDP: true, defaultEntry: action);
|
|
}
|
|
|
|
@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<MenuEntryBase<String>> menuItems = [
|
|
_connectAction(context),
|
|
_transferFileAction(context),
|
|
_viewCameraAction(context),
|
|
_terminalAction(context),
|
|
];
|
|
|
|
if (peer.platform == kPeerPlatformWindows) {
|
|
menuItems.add(_terminalRunAsAdminAction(context));
|
|
}
|
|
|
|
final List favs = (await bind.mainGetFav()).toList();
|
|
|
|
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
|
menuItems.add(_tcpTunnelingAction(context));
|
|
}
|
|
// menuItems.add(await _openNewConnInOptAction(peer.id));
|
|
if (!isWeb) {
|
|
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
|
}
|
|
if (isWindows && peer.platform == kPeerPlatformWindows) {
|
|
menuItems.add(_rdpAction(context, peer.id));
|
|
}
|
|
if (isWindows) {
|
|
menuItems.add(_createShortCutAction(peer.id));
|
|
}
|
|
menuItems.add(MenuEntryDivider());
|
|
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 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));
|
|
}
|
|
// menuItems.add(await _openNewConnInOptAction(peer.id));
|
|
if (!isWeb) {
|
|
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
|
}
|
|
if (isWindows && peer.platform == kPeerPlatformWindows) {
|
|
menuItems.add(_rdpAction(context, peer.id));
|
|
}
|
|
if (isWindows) {
|
|
menuItems.add(_createShortCutAction(peer.id));
|
|
}
|
|
menuItems.add(MenuEntryDivider());
|
|
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<MenuEntryBase<String>> menuItems = [
|
|
_connectAction(context),
|
|
_transferFileAction(context),
|
|
_viewCameraAction(context),
|
|
_terminalAction(context),
|
|
];
|
|
|
|
if (peer.platform == kPeerPlatformWindows) {
|
|
menuItems.add(_terminalRunAsAdminAction(context));
|
|
}
|
|
|
|
final List favs = (await bind.mainGetFav()).toList();
|
|
|
|
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
|
menuItems.add(_tcpTunnelingAction(context));
|
|
}
|
|
// menuItems.add(await _openNewConnInOptAction(peer.id));
|
|
if (!isWeb) {
|
|
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
|
}
|
|
if (isWindows && peer.platform == kPeerPlatformWindows) {
|
|
menuItems.add(_rdpAction(context, peer.id));
|
|
}
|
|
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 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));
|
|
}
|
|
// menuItems.add(await _openNewConnInOptAction(peer.id));
|
|
if (!isWeb) {
|
|
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
|
}
|
|
if (isWindows && peer.platform == kPeerPlatformWindows) {
|
|
menuItems.add(_rdpAction(context, peer.id));
|
|
}
|
|
if (isWindows) {
|
|
menuItems.add(_createShortCutAction(peer.id));
|
|
}
|
|
if (gFFI.abModel.current.canWrite()) {
|
|
menuItems.add(MenuEntryDivider());
|
|
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 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));
|
|
}
|
|
// menuItems.add(await _openNewConnInOptAction(peer.id));
|
|
if (!isWeb) {
|
|
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
|
}
|
|
if (isWindows && peer.platform == kPeerPlatformWindows) {
|
|
menuItems.add(_rdpAction(context, peer.id));
|
|
}
|
|
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);
|
|
}
|