Merge pull request #1426 from fufesou/flutter_desktop_new_remote_menu_2

Flutter desktop new remote menu 2
This commit is contained in:
RustDesk 2022-09-02 10:04:22 +08:00 committed by GitHub
commit 06541be459
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1046 additions and 721 deletions

2
flutter/.gitignore vendored
View File

@ -48,7 +48,7 @@ lib/generated_bridge.dart
lib/generated_bridge.freezed.dart lib/generated_bridge.freezed.dart
# Flutter Generated Files # Flutter Generated Files
**/flutter/GeneratedPluginRegistrant.swift **/GeneratedPluginRegistrant.swift
**/flutter/generated_plugin_registrant.cc **/flutter/generated_plugin_registrant.cc
**/flutter/generated_plugin_registrant.h **/flutter/generated_plugin_registrant.h
**/flutter/generated_plugins.cmake **/flutter/generated_plugins.cmake

View File

@ -427,7 +427,45 @@ class CustomAlertDialog extends StatelessWidget {
void msgBox( void msgBox(
String type, String title, String text, OverlayDialogManager dialogManager, String type, String title, String text, OverlayDialogManager dialogManager,
{bool? hasCancel}) { {bool? hasCancel}) {
var wrap = (String text, void Function() onPressed) => ButtonTheme( dialogManager.dismissAll();
List<Widget> buttons = [];
if (type != "connecting" && type != "success" && !type.contains("nook")) {
buttons.insert(
0,
msgBoxButton(translate('OK'), () {
dialogManager.dismissAll();
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
if (!type.contains("custom")) {
closeConnection();
}
}));
}
hasCancel ??= !type.contains("error") &&
!type.contains("nocancel") &&
type != "restarting";
if (hasCancel) {
buttons.insert(
0,
msgBoxButton(translate('Cancel'), () {
dialogManager.dismissAll();
}));
}
// TODO: test this button
if (type.contains("hasclose")) {
buttons.insert(
0,
msgBoxButton(translate('Close'), () {
dialogManager.dismissAll();
}));
}
dialogManager.show((setState, close) => CustomAlertDialog(
title: _msgBoxTitle(title),
content: Text(translate(text), style: TextStyle(fontSize: 15)),
actions: buttons));
}
Widget msgBoxButton(String text, void Function() onPressed) {
return ButtonTheme(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
//limits the touch area to the button area //limits the touch area to the button area
@ -439,44 +477,16 @@ void msgBox(
onPressed: onPressed, onPressed: onPressed,
child: child:
Text(translate(text), style: TextStyle(color: MyTheme.accent)))); Text(translate(text), style: TextStyle(color: MyTheme.accent))));
}
Widget _msgBoxTitle(String title) => Text(translate(title), style: TextStyle(fontSize: 21));
void msgBoxCommon(OverlayDialogManager dialogManager, String title,
Widget content, List<Widget> buttons) {
dialogManager.dismissAll(); dialogManager.dismissAll();
List<Widget> buttons = [];
if (type != "connecting" && type != "success" && type.indexOf("nook") < 0) {
buttons.insert(
0,
wrap(translate('OK'), () {
dialogManager.dismissAll();
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
if (type.indexOf("custom") < 0) {
closeConnection();
}
}));
}
if (hasCancel == null) {
// hasCancel = type != 'error';
hasCancel = type.indexOf("error") < 0 &&
type.indexOf("nocancel") < 0 &&
type != "restarting";
}
if (hasCancel) {
buttons.insert(
0,
wrap(translate('Cancel'), () {
dialogManager.dismissAll();
}));
}
// TODO: test this button
if (type.indexOf("hasclose") >= 0) {
buttons.insert(
0,
wrap(translate('Close'), () {
dialogManager.dismissAll();
}));
}
dialogManager.show((setState, close) => CustomAlertDialog( dialogManager.show((setState, close) => CustomAlertDialog(
title: Text(translate(title), style: TextStyle(fontSize: 21)), title: _msgBoxTitle(title),
content: Text(translate(text), style: TextStyle(fontSize: 15)), content: content,
actions: buttons)); actions: buttons));
} }
@ -495,13 +505,13 @@ const G = M * K;
String readableFileSize(double size) { String readableFileSize(double size) {
if (size < K) { if (size < K) {
return size.toStringAsFixed(2) + " B"; return "${size.toStringAsFixed(2)} B";
} else if (size < M) { } else if (size < M) {
return (size / K).toStringAsFixed(2) + " KB"; return "${(size / K).toStringAsFixed(2)} KB";
} else if (size < G) { } else if (size < G) {
return (size / M).toStringAsFixed(2) + " MB"; return "${(size / M).toStringAsFixed(2)} MB";
} else { } else {
return (size / G).toStringAsFixed(2) + " GB"; return "${(size / G).toStringAsFixed(2)} GB";
} }
} }

View File

@ -8,7 +8,10 @@ const String kAppTypeDesktopPortForward = "port forward";
const String kTabLabelHomePage = "Home"; const String kTabLabelHomePage = "Home";
const String kTabLabelSettingPage = "Settings"; const String kTabLabelSettingPage = "Settings";
const int kDefaultDisplayWidth = 1280; const int kMobileDefaultDisplayWidth = 720;
const int kDefaultDisplayHeight = 720; const int kMobileDefaultDisplayHeight = 1280;
const int kDesktopDefaultDisplayWidth = 1080;
const int kDesktopDefaultDisplayHeight = 720;
const kInvalidValueStr = "InvalidValueStr"; const kInvalidValueStr = "InvalidValueStr";

View File

@ -33,7 +33,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
final _idController = TextEditingController(); final _idController = TextEditingController();
/// Update url. If it's not null, means an update is available. /// Update url. If it's not null, means an update is available.
var _updateUrl = ''; final _updateUrl = '';
Timer? _updateTimer; Timer? _updateTimer;
@ -92,7 +92,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
if (snapshot.hasData) { if (snapshot.hasData) {
return snapshot.data!; return snapshot.data!;
} else { } else {
return Offstage(); return const Offstage();
} }
}), }),
], ],
@ -110,7 +110,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
/// Callback for the connect button. /// Callback for the connect button.
/// Connects to the selected peer. /// Connects to the selected peer.
void onConnect({bool isFileTransfer = false}) { void onConnect({bool isFileTransfer = false}) {
var id = _idController.text.trim(); final id = _idController.text.trim();
connect(id, isFileTransfer: isFileTransfer); connect(id, isFileTransfer: isFileTransfer);
} }
@ -120,9 +120,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
if (id == '') return; if (id == '') return;
id = id.replaceAll(' ', ''); id = id.replaceAll(' ', '');
if (isFileTransfer) { if (isFileTransfer) {
await rustDeskWinManager.new_file_transfer(id); await rustDeskWinManager.newFileTransfer(id);
} else { } else {
await rustDeskWinManager.new_remote_desktop(id); await rustDeskWinManager.newRemoteDesktop(id);
} }
FocusScopeNode currentFocus = FocusScope.of(context); FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) { if (!currentFocus.hasPrimaryFocus) {

View File

@ -61,6 +61,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final args = jsonDecode(call.arguments); final args = jsonDecode(call.arguments);
final id = args['id']; final id = args['id'];
window_on_top(windowId()); window_on_top(windowId());
ConnectionTypeState.init(id);
tabController.add(TabInfo( tabController.add(TabInfo(
key: id, key: id,
label: id, label: id,
@ -108,7 +109,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
}, },
tabBuilder: (key, icon, label, themeConf) => Obx(() { tabBuilder: (key, icon, label, themeConf) => Obx(() {
final connectionType = ConnectionTypeState.find(key); final connectionType = ConnectionTypeState.find(key);
if (!ConnectionTypeState.find(key).isValid()) { if (!connectionType.isValid()) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [

View File

@ -689,11 +689,11 @@ class ImagePaint extends StatelessWidget {
width: c.getDisplayWidth() * s, width: c.getDisplayWidth() * s,
height: c.getDisplayHeight() * s, height: c.getDisplayHeight() * s,
child: CustomPaint( child: CustomPaint(
painter: new ImagePainter(image: m.image, x: 0, y: 0, scale: s), painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
)); ));
return Center( return Center(
child: NotificationListener<ScrollNotification>( child: NotificationListener<ScrollNotification>(
onNotification: (_notification) { onNotification: (notification) {
final percentX = _horizontal.position.extentBefore / final percentX = _horizontal.position.extentBefore /
(_horizontal.position.extentBefore + (_horizontal.position.extentBefore +
_horizontal.position.extentInside + _horizontal.position.extentInside +
@ -716,8 +716,8 @@ class ImagePaint extends StatelessWidget {
width: c.size.width, width: c.size.width,
height: c.size.height, height: c.size.height,
child: CustomPaint( child: CustomPaint(
painter: new ImagePainter( painter:
image: m.image, x: c.x / s, y: c.y / s, scale: s), ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
)); ));
return _buildListener(imageWidget); return _buildListener(imageWidget);
} }
@ -771,7 +771,7 @@ class CursorPaint extends StatelessWidget {
// final adjust = m.adjustForKeyboard(); // final adjust = m.adjustForKeyboard();
var s = c.scale; var s = c.scale;
return CustomPaint( return CustomPaint(
painter: new ImagePainter( painter: ImagePainter(
image: m.image, image: m.image,
x: m.x * s - m.hotx + c.x, x: m.x * s - m.hotx + c.x,
y: m.y * s - m.hoty + c.y, y: m.y * s - m.hoty + c.y,
@ -796,15 +796,16 @@ class ImagePainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
if (image == null) return; if (image == null) return;
if (x.isNaN || y.isNaN) return;
canvas.scale(scale, scale); canvas.scale(scale, scale);
// https://github.com/flutter/flutter/issues/76187#issuecomment-784628161 // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161
// https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html
var paint = new Paint(); var paint = Paint();
paint.filterQuality = FilterQuality.medium; paint.filterQuality = FilterQuality.medium;
if (scale > 10.00000) { if (scale > 10.00000) {
paint.filterQuality = FilterQuality.high; paint.filterQuality = FilterQuality.high;
} }
canvas.drawImage(image!, new Offset(x, y), paint); canvas.drawImage(image!, Offset(x, y), paint);
} }
@override @override

View File

@ -21,18 +21,16 @@ final peerSearchTextController =
TextEditingController(text: peerSearchText.value); TextEditingController(text: peerSearchText.value);
class _PeerWidget extends StatefulWidget { class _PeerWidget extends StatefulWidget {
late final _peers; final Peers peers;
late final OffstageFunc _offstageFunc; final OffstageFunc offstageFunc;
late final PeerCardWidgetFunc _peerCardWidgetFunc; final PeerCardWidgetFunc peerCardWidgetFunc;
_PeerWidget(Peers peers, OffstageFunc offstageFunc, const _PeerWidget(
PeerCardWidgetFunc peerCardWidgetFunc, {required this.peers,
{Key? key}) required this.offstageFunc,
: super(key: key) { required this.peerCardWidgetFunc,
_peers = peers; Key? key})
_offstageFunc = offstageFunc; : super(key: key);
_peerCardWidgetFunc = peerCardWidgetFunc;
}
@override @override
_PeerWidgetState createState() => _PeerWidgetState(); _PeerWidgetState createState() => _PeerWidgetState();
@ -42,9 +40,9 @@ class _PeerWidget extends StatefulWidget {
class _PeerWidgetState extends State<_PeerWidget> with WindowListener { class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
static const int _maxQueryCount = 3; static const int _maxQueryCount = 3;
var _curPeers = Set<String>(); final _curPeers = <String>{};
var _lastChangeTime = DateTime.now(); var _lastChangeTime = DateTime.now();
var _lastQueryPeers = Set<String>(); var _lastQueryPeers = <String>{};
var _lastQueryTime = DateTime.now().subtract(Duration(hours: 1)); var _lastQueryTime = DateTime.now().subtract(Duration(hours: 1));
var _queryCoun = 0; var _queryCoun = 0;
var _exit = false; var _exit = false;
@ -78,65 +76,62 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final space = 12.0; const space = 12.0;
return ChangeNotifierProvider<Peers>( return ChangeNotifierProvider<Peers>(
create: (context) => super.widget._peers, create: (context) => widget.peers,
child: Consumer<Peers>( child: Consumer<Peers>(
builder: (context, peers, child) => peers.peers.isEmpty builder: (context, peers, child) => peers.peers.isEmpty
? Center( ? Center(
child: Text(translate("Empty")), child: Text(translate("Empty")),
) )
: SingleChildScrollView( : SingleChildScrollView(
child: ObxValue<RxString>((searchText) { child: ObxValue<RxString>((searchText) {
return FutureBuilder<List<Peer>>( return FutureBuilder<List<Peer>>(
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
final peers = snapshot.data!; final peers = snapshot.data!;
final cards = <Widget>[]; final cards = <Widget>[];
for (final peer in peers) { for (final peer in peers) {
cards.add(Offstage( cards.add(Offstage(
key: ValueKey("off${peer.id}"), key: ValueKey("off${peer.id}"),
offstage: super.widget._offstageFunc(peer), offstage: widget.offstageFunc(peer),
child: Obx( child: Obx(
() => SizedBox( () => SizedBox(
width: 220, width: 220,
height: height:
peerCardUiType.value == PeerUiType.grid peerCardUiType.value == PeerUiType.grid
? 140 ? 140
: 42, : 42,
child: VisibilityDetector( child: VisibilityDetector(
key: ValueKey(peer.id), key: ValueKey(peer.id),
onVisibilityChanged: (info) { onVisibilityChanged: (info) {
final peerId = final peerId =
(info.key as ValueKey).value; (info.key as ValueKey).value;
if (info.visibleFraction > 0.00001) { if (info.visibleFraction > 0.00001) {
_curPeers.add(peerId); _curPeers.add(peerId);
} else { } else {
_curPeers.remove(peerId); _curPeers.remove(peerId);
} }
_lastChangeTime = DateTime.now(); _lastChangeTime = DateTime.now();
}, },
child: super child: widget.peerCardWidgetFunc(peer),
.widget
._peerCardWidgetFunc(peer),
),
), ),
))); ),
} )));
return Wrap(
spacing: space,
runSpacing: space,
children: cards);
} else {
return const Center(
child: CircularProgressIndicator(),
);
} }
}, return Wrap(
future: matchPeers(searchText.value, peers.peers), spacing: space, runSpacing: space, children: cards);
); } else {
}, peerSearchText), return const Center(
)), child: CircularProgressIndicator(),
);
}
},
future: matchPeers(searchText.value, peers.peers),
);
}, peerSearchText),
),
),
); );
} }
@ -175,31 +170,42 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
} }
abstract class BasePeerWidget extends StatelessWidget { abstract class BasePeerWidget extends StatelessWidget {
late final _name; final String name;
late final _loadEvent; final String loadEvent;
late final OffstageFunc _offstageFunc; final OffstageFunc offstageFunc;
late final PeerCardWidgetFunc _peerCardWidgetFunc; final PeerCardWidgetFunc peerCardWidgetFunc;
late final List<Peer> _initPeers; final List<Peer> initPeers;
BasePeerWidget({Key? key}) : super(key: key) {} const BasePeerWidget({
Key? key,
required this.name,
required this.loadEvent,
required this.offstageFunc,
required this.peerCardWidgetFunc,
required this.initPeers,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _PeerWidget(Peers(_name, _loadEvent, _initPeers), _offstageFunc, return _PeerWidget(
_peerCardWidgetFunc); peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers),
offstageFunc: offstageFunc,
peerCardWidgetFunc: peerCardWidgetFunc);
} }
} }
class RecentPeerWidget extends BasePeerWidget { class RecentPeerWidget extends BasePeerWidget {
RecentPeerWidget({Key? key}) : super(key: key) { RecentPeerWidget({Key? key})
super._name = "recent peer"; : super(
super._loadEvent = "load_recent_peers"; key: key,
super._offstageFunc = (Peer _peer) => false; name: 'recent peer',
super._peerCardWidgetFunc = (Peer peer) => RecentPeerCard( loadEvent: 'load_recent_peers',
peer: peer, offstageFunc: (Peer peer) => false,
peerCardWidgetFunc: (Peer peer) => RecentPeerCard(
peer: peer,
),
initPeers: [],
); );
super._initPeers = [];
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -210,13 +216,17 @@ class RecentPeerWidget extends BasePeerWidget {
} }
class FavoritePeerWidget extends BasePeerWidget { class FavoritePeerWidget extends BasePeerWidget {
FavoritePeerWidget({Key? key}) : super(key: key) { FavoritePeerWidget({Key? key})
super._name = "favorite peer"; : super(
super._loadEvent = "load_fav_peers"; key: key,
super._offstageFunc = (Peer _peer) => false; name: 'favorite peer',
super._peerCardWidgetFunc = (Peer peer) => FavoritePeerCard(peer: peer); loadEvent: 'load_fav_peers',
super._initPeers = []; offstageFunc: (Peer peer) => false,
} peerCardWidgetFunc: (Peer peer) => FavoritePeerCard(
peer: peer,
),
initPeers: [],
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -227,13 +237,17 @@ class FavoritePeerWidget extends BasePeerWidget {
} }
class DiscoveredPeerWidget extends BasePeerWidget { class DiscoveredPeerWidget extends BasePeerWidget {
DiscoveredPeerWidget({Key? key}) : super(key: key) { DiscoveredPeerWidget({Key? key})
super._name = "discovered peer"; : super(
super._loadEvent = "load_lan_peers"; key: key,
super._offstageFunc = (Peer _peer) => false; name: 'discovered peer',
super._peerCardWidgetFunc = (Peer peer) => DiscoveredPeerCard(peer: peer); loadEvent: 'load_lan_peers',
super._initPeers = []; offstageFunc: (Peer peer) => false,
} peerCardWidgetFunc: (Peer peer) => DiscoveredPeerCard(
peer: peer,
),
initPeers: [],
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -244,21 +258,26 @@ class DiscoveredPeerWidget extends BasePeerWidget {
} }
class AddressBookPeerWidget extends BasePeerWidget { class AddressBookPeerWidget extends BasePeerWidget {
AddressBookPeerWidget({Key? key}) : super(key: key) { AddressBookPeerWidget({Key? key})
super._name = "address book peer"; : super(
super._offstageFunc = key: key,
(Peer peer) => !_hitTag(gFFI.abModel.selectedTags, peer.tags); name: 'address book peer',
super._peerCardWidgetFunc = (Peer peer) => AddressBookPeerCard(peer: peer); loadEvent: 'load_address_book_peers',
super._initPeers = _loadPeers(); offstageFunc: (Peer peer) =>
} !_hitTag(gFFI.abModel.selectedTags, peer.tags),
peerCardWidgetFunc: (Peer peer) => DiscoveredPeerCard(
peer: peer,
),
initPeers: _loadPeers(),
);
List<Peer> _loadPeers() { static List<Peer> _loadPeers() {
return gFFI.abModel.peers.map((e) { return gFFI.abModel.peers.map((e) {
return Peer.fromJson(e['id'], e); return Peer.fromJson(e['id'], e);
}).toList(); }).toList();
} }
bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) { static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
if (selectedTags.isEmpty) { if (selectedTags.isEmpty) {
return true; return true;
} }

View File

@ -8,10 +8,18 @@ import '../../common.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/peer_model.dart'; import '../../models/peer_model.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import './material_mod_popup_menu.dart' as mod_menu;
import './popup_menu.dart';
typedef PopupMenuItemsFunc = Future<List<PopupMenuItem<String>>> Function(); class _PopupMenuTheme {
static const Color commonColor = MyTheme.accent;
// kMinInteractiveDimension
static const double height = 25.0;
static const double dividerHeight = 12.0;
}
enum PeerType { recent, fav, discovered, ab } typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
Function(BuildContext);
enum PeerUiType { grid, list } enum PeerUiType { grid, list }
@ -19,14 +27,16 @@ final peerCardUiType = PeerUiType.grid.obs;
class _PeerCard extends StatefulWidget { class _PeerCard extends StatefulWidget {
final Peer peer; final Peer peer;
final PopupMenuItemsFunc popupMenuItemsFunc; final RxString alias;
final PeerType type; final Function(BuildContext, String) connect;
final PopupMenuEntryBuilder popupMenuEntryBuilder;
_PeerCard( _PeerCard(
{required this.peer, {required this.peer,
required this.popupMenuItemsFunc, required this.alias,
Key? key, required this.connect,
required this.type}) required this.popupMenuEntryBuilder,
Key? key})
: super(key: key); : super(key: key);
@override @override
@ -36,7 +46,6 @@ class _PeerCard extends StatefulWidget {
/// State for the connection page. /// State for the connection page.
class _PeerCardState extends State<_PeerCard> class _PeerCardState extends State<_PeerCard>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
var _menuPos = RelativeRect.fill;
final double _cardRadis = 20; final double _cardRadis = 20;
final double _borderWidth = 2; final double _borderWidth = 2;
final RxBool _iconMoreHover = false.obs; final RxBool _iconMoreHover = false.obs;
@ -66,7 +75,7 @@ class _PeerCardState extends State<_PeerCard>
: null); : null);
}, },
child: GestureDetector( child: GestureDetector(
onDoubleTap: () => _connect(peer.id), onDoubleTap: () => widget.connect(context, peer.id),
child: Obx(() => peerCardUiType.value == PeerUiType.grid child: Obx(() => peerCardUiType.value == PeerUiType.grid
? _buildPeerCard(context, peer, deco) ? _buildPeerCard(context, peer, deco)
: _buildPeerTile(context, peer, deco))), : _buildPeerTile(context, peer, deco))),
@ -185,46 +194,28 @@ class _PeerCardState extends State<_PeerCard>
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(6), padding: const EdgeInsets.all(6),
child: child: _getPlatformImage(peer.platform, 60),
_getPlatformImage('${peer.platform}', 60),
), ),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: FutureBuilder<String>( child: Obx(() {
future: bind.mainGetPeerOption( final name = widget.alias.value.isEmpty
id: peer.id, key: 'alias'), ? '${peer.username}@${peer.hostname}'
builder: (_, snapshot) { : widget.alias.value;
if (snapshot.hasData) { return Tooltip(
final name = snapshot.data!.isEmpty message: name,
? '${peer.username}@${peer.hostname}' waitDuration: Duration(seconds: 1),
: snapshot.data!; child: Text(
return Tooltip( name,
message: name, style: TextStyle(
waitDuration: Duration(seconds: 1), color: Colors.white70,
child: Text( fontSize: 12),
name, textAlign: TextAlign.center,
style: TextStyle( overflow: TextOverflow.ellipsis,
color: Colors.white70, ),
fontSize: 12), );
textAlign: TextAlign.center, }),
overflow: TextOverflow.ellipsis,
),
);
} else {
// alias has not arrived
return Center(
child: Text(
'${peer.username}@${peer.hostname}',
style: TextStyle(
color: Colors.white70,
fontSize: 12),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
));
}
},
),
), ),
], ],
), ),
@ -248,7 +239,7 @@ class _PeerCardState extends State<_PeerCard>
backgroundColor: peer.online backgroundColor: peer.online
? Colors.green ? Colors.green
: Colors.yellow)), : Colors.yellow)),
Text('${peer.id}') Text(peer.id)
]).paddingSymmetric(vertical: 8), ]).paddingSymmetric(vertical: 8),
_actionMore(peer), _actionMore(peer),
], ],
@ -262,32 +253,93 @@ class _PeerCardState extends State<_PeerCard>
); );
} }
Widget _actionMore(Peer peer) => Listener( Widget _actionMore(Peer peer) {
onPointerDown: (e) { return FutureBuilder(
final x = e.position.dx; future: widget.popupMenuEntryBuilder(context),
final y = e.position.dy; initialData: const <mod_menu.PopupMenuEntry<String>>[],
_menuPos = RelativeRect.fromLTRB(x, y, x, y); builder: (BuildContext context,
}, AsyncSnapshot<List<mod_menu.PopupMenuEntry<String>>> snapshot) {
onPointerUp: (_) => _showPeerMenu(context, peer.id), if (snapshot.hasData) {
child: MouseRegion( return Listener(
onEnter: (_) => _iconMoreHover.value = true, child: MouseRegion(
onExit: (_) => _iconMoreHover.value = false, onEnter: (_) => _iconMoreHover.value = true,
child: CircleAvatar( onExit: (_) => _iconMoreHover.value = false,
radius: 14, child: CircleAvatar(
backgroundColor: _iconMoreHover.value radius: 14,
? MyTheme.color(context).grayBg! backgroundColor: _iconMoreHover.value
: MyTheme.color(context).bg!, ? MyTheme.color(context).grayBg!
child: Icon(Icons.more_vert, : MyTheme.color(context).bg!,
size: 18, child: mod_menu.PopupMenuButton(
color: _iconMoreHover.value padding: EdgeInsets.zero,
? MyTheme.color(context).text icon: Icon(Icons.more_vert,
: MyTheme.color(context).lightText)))); size: 18,
color: _iconMoreHover.value
? MyTheme.color(context).text
: MyTheme.color(context).lightText),
position: mod_menu.PopupMenuPosition.under,
itemBuilder: (BuildContext context) => snapshot.data!,
))));
} else {
return Container();
}
});
}
/// Get the image for the current [platform].
Widget _getPlatformImage(String platform, double size) {
platform = platform.toLowerCase();
if (platform == 'mac os') {
platform = 'mac';
} else if (platform != 'linux' && platform != 'android') {
platform = 'win';
}
return Image.asset('assets/$platform.png', height: size, width: size);
}
@override
bool get wantKeepAlive => true;
}
abstract class BasePeerCard extends StatelessWidget {
final RxString alias = ''.obs;
final Peer peer;
BasePeerCard({required this.peer, Key? key}) : super(key: key) {
bind
.mainGetPeerOption(id: peer.id, key: 'alias')
.then((value) => alias.value = value);
}
@override
Widget build(BuildContext context) {
return _PeerCard(
peer: peer,
alias: alias,
connect: (BuildContext context, String id) => _connect(context, id),
popupMenuEntryBuilder: _buildPopupMenuEntry,
);
}
Future<List<mod_menu.PopupMenuEntry<String>>> _buildPopupMenuEntry(
BuildContext context) async =>
(await _buildMenuItems(context))
.map((e) => e.build(
context,
const MenuConfig(
commonColor: _PopupMenuTheme.commonColor,
height: _PopupMenuTheme.height,
dividerHeight: _PopupMenuTheme.dividerHeight)))
.expand((i) => i)
.toList();
@protected
Future<List<MenuEntryBase<String>>> _buildMenuItems(BuildContext context);
/// Connect to a peer with [id]. /// Connect to a peer with [id].
/// If [isFileTransfer], starts a session only for file transfer. /// If [isFileTransfer], starts a session only for file transfer.
/// If [isTcpTunneling], starts a session only for tcp tunneling. /// If [isTcpTunneling], starts a session only for tcp tunneling.
/// If [isRDP], starts a session only for rdp. /// If [isRDP], starts a session only for rdp.
void _connect(String id, void _connect(BuildContext context, String id,
{bool isFileTransfer = false, {bool isFileTransfer = false,
bool isTcpTunneling = false, bool isTcpTunneling = false,
bool isRDP = false}) async { bool isRDP = false}) async {
@ -296,11 +348,11 @@ class _PeerCardState extends State<_PeerCard>
assert(!(isFileTransfer && isTcpTunneling && isRDP), assert(!(isFileTransfer && isTcpTunneling && isRDP),
"more than one connect type"); "more than one connect type");
if (isFileTransfer) { if (isFileTransfer) {
await rustDeskWinManager.new_file_transfer(id); await rustDeskWinManager.newFileTransfer(id);
} else if (isTcpTunneling || isRDP) { } else if (isTcpTunneling || isRDP) {
await rustDeskWinManager.new_port_forward(id, isRDP); await rustDeskWinManager.newPortForward(id, isRDP);
} else { } else {
await rustDeskWinManager.new_remote_desktop(id); await rustDeskWinManager.newRemoteDesktop(id);
} }
FocusScopeNode currentFocus = FocusScope.of(context); FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) { if (!currentFocus.hasPrimaryFocus) {
@ -308,105 +360,377 @@ class _PeerCardState extends State<_PeerCard>
} }
} }
/// Show the peer menu and handle user's choice. MenuEntryBase<String> _connectCommonAction(
/// User might remove the peer or send a file to the peer. BuildContext context, String id, String title,
void _showPeerMenu(BuildContext context, String id) async { {bool isFileTransfer = false,
var value = await showMenu( bool isTcpTunneling = false,
context: context, bool isRDP = false}) {
position: _menuPos, return MenuEntryButton<String>(
items: await super.widget.popupMenuItemsFunc(), childBuilder: (TextStyle? style) => Text(
elevation: 8, translate(title),
); style: style,
if (value == 'connect') {
_connect(id);
} else if (value == 'file') {
_connect(id, isFileTransfer: true);
} else if (value == 'tcp-tunnel') {
_connect(id, isTcpTunneling: true);
} else if (value == 'RDP') {
_connect(id, isRDP: true);
} else if (value == 'remove') {
await bind.mainRemovePeer(id: id);
removePreference(id);
Get.forceAppUpdate(); // TODO use inner model / state
} else if (value == 'add-fav') {
final favs = (await bind.mainGetFav()).toList();
if (favs.indexOf(id) < 0) {
favs.add(id);
bind.mainStoreFav(favs: favs);
}
} else if (value == 'remove-fav') {
final favs = (await bind.mainGetFav()).toList();
if (favs.remove(id)) {
bind.mainStoreFav(favs: favs);
Get.forceAppUpdate(); // TODO use inner model / state
}
} else if (value == 'ab-delete') {
gFFI.abModel.deletePeer(id);
await gFFI.abModel.updateAb();
setState(() {});
} else if (value == 'ab-edit-tag') {
_abEditTag(id);
} else if (value == 'rename') {
_rename(id);
} else if (value == 'unremember-password') {
await bind.mainForgetPassword(id: id);
} else if (value == 'force-always-relay') {
String value;
String oldValue =
await bind.mainGetPeerOption(id: id, key: 'force-always-relay');
if (oldValue.isEmpty) {
value = 'Y';
} else {
value = '';
}
await bind.mainSetPeerOption(
id: id, key: 'force-always-relay', value: value);
}
}
Widget _buildTag(String tagName, RxList<dynamic> rxTags,
{Function()? onTap}) {
return ContextMenuArea(
width: 100,
builder: (context) => [
ListTile(
title: Text(translate("Delete")),
onTap: () {
gFFI.abModel.deleteTag(tagName);
gFFI.abModel.updateAb();
Future.delayed(Duration.zero, () => Get.back());
},
)
],
child: GestureDetector(
onTap: onTap,
child: Obx(
() => Container(
decoration: BoxDecoration(
color: rxTags.contains(tagName) ? Colors.blue : null,
border: Border.all(color: MyTheme.darkGray),
borderRadius: BorderRadius.circular(10)),
margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0),
padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0),
child: Text(
tagName,
style: TextStyle(
color: rxTags.contains(tagName) ? MyTheme.white : null),
),
),
),
), ),
proc: () {
_connect(
context,
peer.id,
isFileTransfer: isFileTransfer,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP,
);
},
dismissOnClicked: true,
); );
} }
/// Get the image for the current [platform]. @protected
Widget _getPlatformImage(String platform, double size) { MenuEntryBase<String> _connectAction(BuildContext context, String id) {
platform = platform.toLowerCase(); return _connectCommonAction(context, id, 'Connect');
if (platform == 'mac os') }
platform = 'mac';
else if (platform != 'linux' && platform != 'android') platform = 'win'; @protected
return Image.asset('assets/$platform.png', height: size, width: size); MenuEntryBase<String> _transferFileAction(BuildContext context, String id) {
return _connectCommonAction(
context,
id,
'Transfer File',
isFileTransfer: true,
);
}
@protected
MenuEntryBase<String> _tcpTunnelingAction(BuildContext context, String id) {
return _connectCommonAction(
context,
id,
'TCP Tunneling',
isTcpTunneling: true,
);
}
@protected
MenuEntryBase<String> _rdpAction(BuildContext context, String id) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Row(
children: [
Text(
translate('RDP'),
style: style,
),
SizedBox(width: 20),
IconButton(
icon: Icon(Icons.edit),
onPressed: () => _rdpDialog(id),
)
],
),
proc: () {
_connect(context, id, isRDP: true);
},
dismissOnClicked: true,
);
}
@protected
Future<MenuEntryBase<String>> _forceAlwaysRelayAction(String id) async {
const option = 'force-always-relay';
return MenuEntrySwitch<String>(
text: translate('Always connect via relay'),
getter: () async {
return (await bind.mainGetPeerOption(id: id, key: option)).isNotEmpty;
},
setter: (bool v) async {
String value;
String oldValue = await bind.mainGetPeerOption(id: id, key: option);
if (oldValue.isEmpty) {
value = 'Y';
} else {
value = '';
}
await bind.mainSetPeerOption(id: id, key: option, value: value);
},
dismissOnClicked: true,
);
}
@protected
MenuEntryBase<String> _renameAction(String id, bool isAddressBook) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Rename'),
style: style,
),
proc: () {
_rename(id, isAddressBook);
},
dismissOnClicked: true,
);
}
@protected
MenuEntryBase<String> _removeAction(
String id, Future<void> Function() reloadFunc) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Remove'),
style: style,
),
proc: () {
() async {
await bind.mainRemovePeer(id: id);
removePreference(id);
await reloadFunc();
// Get.forceAppUpdate(); // TODO use inner model / state
}();
},
dismissOnClicked: true,
);
}
@protected
MenuEntryBase<String> _unrememberPasswordAction(String id) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Unremember Password'),
style: style,
),
proc: () {
bind.mainForgetPassword(id: id);
},
dismissOnClicked: true,
);
}
@protected
MenuEntryBase<String> _addFavAction(String id) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Add to Favorites'),
style: style,
),
proc: () {
() async {
final favs = (await bind.mainGetFav()).toList();
if (!favs.contains(id)) {
favs.add(id);
bind.mainStoreFav(favs: favs);
}
}();
},
dismissOnClicked: true,
);
}
@protected
MenuEntryBase<String> _rmFavAction(String id) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Remove from Favorites'),
style: style,
),
proc: () {
() async {
final favs = (await bind.mainGetFav()).toList();
if (favs.remove(id)) {
bind.mainStoreFav(favs: favs);
Get.forceAppUpdate(); // TODO use inner model / state
}
}();
},
dismissOnClicked: true,
);
}
void _rename(String id, bool isAddressBook) async {
RxBool isInProgress = false.obs;
var name = await bind.mainGetPeerOption(id: id, key: 'alias');
var controller = TextEditingController(text: name);
if (isAddressBook) {
final peer = gFFI.abModel.peers.firstWhere((p) => id == p['id']);
if (peer == null) {
// this should not happen
} else {
name = peer['alias'] ?? '';
}
}
gFFI.dialogManager.show((setState, close) {
return CustomAlertDialog(
title: Text(translate('Rename')),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Form(
child: TextFormField(
controller: controller,
decoration: InputDecoration(border: OutlineInputBorder()),
),
),
),
Obx(() => Offstage(
offstage: isInProgress.isFalse,
child: LinearProgressIndicator())),
],
),
actions: [
TextButton(
onPressed: () {
close();
},
child: Text(translate("Cancel"))),
TextButton(
onPressed: () async {
isInProgress.value = true;
name = controller.text;
await bind.mainSetPeerOption(id: id, key: 'alias', value: name);
if (isAddressBook) {
gFFI.abModel.setPeerOption(id, 'alias', name);
await gFFI.abModel.updateAb();
}
alias.value =
await bind.mainGetPeerOption(id: peer.id, key: 'alias');
close();
isInProgress.value = false;
},
child: Text(translate("OK"))),
],
);
});
}
}
class RecentPeerCard extends BasePeerCard {
RecentPeerCard({required Peer peer, Key? key}) : super(peer: peer, key: key);
@override
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer.id),
_transferFileAction(context, peer.id),
_tcpTunnelingAction(context, peer.id),
];
if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id));
}
menuItems.add(await _forceAlwaysRelayAction(peer.id));
menuItems.add(_renameAction(peer.id, false));
menuItems.add(_removeAction(peer.id, () async {
await bind.mainLoadRecentPeers();
}));
menuItems.add(_unrememberPasswordAction(peer.id));
menuItems.add(_addFavAction(peer.id));
return menuItems;
}
}
class FavoritePeerCard extends BasePeerCard {
FavoritePeerCard({required Peer peer, Key? key})
: super(peer: peer, key: key);
@override
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer.id),
_transferFileAction(context, peer.id),
_tcpTunnelingAction(context, peer.id),
];
if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id));
}
menuItems.add(await _forceAlwaysRelayAction(peer.id));
menuItems.add(_renameAction(peer.id, false));
menuItems.add(_removeAction(peer.id, () async {
await bind.mainLoadFavPeers();
}));
menuItems.add(_unrememberPasswordAction(peer.id));
menuItems.add(_rmFavAction(peer.id));
return menuItems;
}
}
class DiscoveredPeerCard extends BasePeerCard {
DiscoveredPeerCard({required Peer peer, Key? key})
: super(peer: peer, key: key);
@override
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer.id),
_transferFileAction(context, peer.id),
_tcpTunnelingAction(context, peer.id),
];
if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id));
}
menuItems.add(await _forceAlwaysRelayAction(peer.id));
menuItems.add(_renameAction(peer.id, false));
menuItems.add(_removeAction(peer.id, () async {
await bind.mainLoadLanPeers();
}));
menuItems.add(_unrememberPasswordAction(peer.id));
return menuItems;
}
}
class AddressBookPeerCard extends BasePeerCard {
AddressBookPeerCard({required Peer peer, Key? key})
: super(peer: peer, key: key);
@override
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer.id),
_transferFileAction(context, peer.id),
_tcpTunnelingAction(context, peer.id),
];
if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id));
}
menuItems.add(await _forceAlwaysRelayAction(peer.id));
menuItems.add(_renameAction(peer.id, false));
menuItems.add(_removeAction(peer.id, () async {}));
menuItems.add(_unrememberPasswordAction(peer.id));
menuItems.add(_addFavAction(peer.id));
menuItems.add(_editTagAction(peer.id));
return menuItems;
}
@protected
@override
MenuEntryBase<String> _removeAction(
String id, Future<void> Function() reloadFunc) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Remove'),
style: style,
),
proc: () {
() async {
gFFI.abModel.deletePeer(id);
await gFFI.abModel.updateAb();
}();
},
dismissOnClicked: true,
);
}
@protected
MenuEntryBase<String> _editTagAction(String id) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Edit Tag'),
style: style,
),
proc: () {
_abEditTag(id);
},
dismissOnClicked: true,
);
} }
void _abEditTag(String id) { void _abEditTag(String id) {
@ -459,205 +783,40 @@ class _PeerCardState extends State<_PeerCard>
}); });
} }
void _rename(String id) async { Widget _buildTag(String tagName, RxList<dynamic> rxTags,
var isInProgress = false; {Function()? onTap}) {
var name = await bind.mainGetPeerOption(id: id, key: 'alias'); return ContextMenuArea(
var controller = TextEditingController(text: name); width: 100,
if (widget.type == PeerType.ab) { builder: (context) => [
final peer = gFFI.abModel.peers.firstWhere((p) => id == p['id']); ListTile(
if (peer == null) { title: Text(translate("Delete")),
// this should not happen onTap: () {
} else { gFFI.abModel.deleteTag(tagName);
name = peer['alias'] ?? ""; gFFI.abModel.updateAb();
} Future.delayed(Duration.zero, () => Get.back());
} },
gFFI.dialogManager.show((setState, close) { )
return CustomAlertDialog( ],
title: Text(translate("Rename")), child: GestureDetector(
content: Column( onTap: onTap,
crossAxisAlignment: CrossAxisAlignment.start, child: Obx(
children: [ () => Container(
Container( decoration: BoxDecoration(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), color: rxTags.contains(tagName) ? Colors.blue : null,
child: Form( border: Border.all(color: MyTheme.darkGray),
child: TextFormField( borderRadius: BorderRadius.circular(10)),
controller: controller, margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0),
decoration: InputDecoration(border: OutlineInputBorder()), padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0),
), child: Text(
), tagName,
style: TextStyle(
color: rxTags.contains(tagName) ? MyTheme.white : null),
), ),
Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) ),
],
), ),
actions: [ ),
TextButton(
onPressed: () {
close();
},
child: Text(translate("Cancel"))),
TextButton(
onPressed: () async {
setState(() {
isInProgress = true;
});
name = controller.text;
await bind.mainSetPeerOption(id: id, key: 'alias', value: name);
if (widget.type == PeerType.ab) {
gFFI.abModel.setPeerOption(id, 'alias', name);
await gFFI.abModel.updateAb();
} else {
Future.delayed(Duration.zero, () {
this.setState(() {});
});
}
close();
setState(() {
isInProgress = false;
});
},
child: Text(translate("OK"))),
],
);
});
}
@override
bool get wantKeepAlive => true;
}
abstract class BasePeerCard extends StatelessWidget {
final Peer peer;
final PeerType type;
BasePeerCard({required this.peer, required this.type, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return _PeerCard(
peer: peer,
popupMenuItemsFunc: _getPopupMenuItems,
type: type,
); );
} }
@protected
Future<List<PopupMenuItem<String>>> _getPopupMenuItems();
}
class RecentPeerCard extends BasePeerCard {
RecentPeerCard({required Peer peer, Key? key})
: super(peer: peer, key: key, type: PeerType.recent);
Future<List<PopupMenuItem<String>>> _getPopupMenuItems() async {
var items = [
PopupMenuItem<String>(
child: Text(translate('Connect')), value: 'connect'),
PopupMenuItem<String>(
child: Text(translate('Transfer File')), value: 'file'),
PopupMenuItem<String>(
child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'),
await _forceAlwaysRelayMenuItem(peer.id),
PopupMenuItem<String>(child: Text(translate('Rename')), value: 'rename'),
PopupMenuItem<String>(child: Text(translate('Remove')), value: 'remove'),
PopupMenuItem<String>(
child: Text(translate('Unremember Password')),
value: 'unremember-password'),
PopupMenuItem<String>(
child: Text(translate('Add to Favorites')), value: 'add-fav'),
];
if (peer.platform == 'Windows') {
items.insert(3, _rdpMenuItem(peer.id));
}
return items;
}
}
class FavoritePeerCard extends BasePeerCard {
FavoritePeerCard({required Peer peer, Key? key})
: super(peer: peer, key: key, type: PeerType.fav);
Future<List<PopupMenuItem<String>>> _getPopupMenuItems() async {
var items = [
PopupMenuItem<String>(
child: Text(translate('Connect')), value: 'connect'),
PopupMenuItem<String>(
child: Text(translate('Transfer File')), value: 'file'),
PopupMenuItem<String>(
child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'),
await _forceAlwaysRelayMenuItem(peer.id),
PopupMenuItem<String>(child: Text(translate('Rename')), value: 'rename'),
PopupMenuItem<String>(child: Text(translate('Remove')), value: 'remove'),
PopupMenuItem<String>(
child: Text(translate('Unremember Password')),
value: 'unremember-password'),
PopupMenuItem<String>(
child: Text(translate('Remove from Favorites')), value: 'remove-fav'),
];
if (peer.platform == 'Windows') {
items.insert(3, _rdpMenuItem(peer.id));
}
return items;
}
}
class DiscoveredPeerCard extends BasePeerCard {
DiscoveredPeerCard({required Peer peer, Key? key})
: super(peer: peer, key: key, type: PeerType.discovered);
Future<List<PopupMenuItem<String>>> _getPopupMenuItems() async {
var items = [
PopupMenuItem<String>(
child: Text(translate('Connect')), value: 'connect'),
PopupMenuItem<String>(
child: Text(translate('Transfer File')), value: 'file'),
PopupMenuItem<String>(
child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'),
await _forceAlwaysRelayMenuItem(peer.id),
PopupMenuItem<String>(child: Text(translate('Rename')), value: 'rename'),
PopupMenuItem<String>(child: Text(translate('Remove')), value: 'remove'),
PopupMenuItem<String>(
child: Text(translate('Unremember Password')),
value: 'unremember-password'),
PopupMenuItem<String>(
child: Text(translate('Add to Favorites')), value: 'add-fav'),
];
if (peer.platform == 'Windows') {
items.insert(3, _rdpMenuItem(peer.id));
}
return items;
}
}
class AddressBookPeerCard extends BasePeerCard {
AddressBookPeerCard({required Peer peer, Key? key})
: super(peer: peer, key: key, type: PeerType.ab);
Future<List<PopupMenuItem<String>>> _getPopupMenuItems() async {
var items = [
PopupMenuItem<String>(
child: Text(translate('Connect')), value: 'connect'),
PopupMenuItem<String>(
child: Text(translate('Transfer File')), value: 'file'),
PopupMenuItem<String>(
child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'),
await _forceAlwaysRelayMenuItem(peer.id),
PopupMenuItem<String>(child: Text(translate('Rename')), value: 'rename'),
PopupMenuItem<String>(
child: Text(translate('Remove')), value: 'ab-delete'),
PopupMenuItem<String>(
child: Text(translate('Unremember Password')),
value: 'unremember-password'),
PopupMenuItem<String>(
child: Text(translate('Add to Favorites')), value: 'add-fav'),
PopupMenuItem<String>(
child: Text(translate('Edit Tag')), value: 'ab-edit-tag'),
];
if (peer.platform == 'Windows') {
items.insert(3, _rdpMenuItem(peer.id));
}
return items;
}
} }
Future<PopupMenuItem<String>> _forceAlwaysRelayMenuItem(String id) async { Future<PopupMenuItem<String>> _forceAlwaysRelayMenuItem(String id) async {

View File

@ -2,7 +2,6 @@ import 'dart:core';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:tuple/tuple.dart';
import './material_mod_popup_menu.dart' as mod_menu; import './material_mod_popup_menu.dart' as mod_menu;
@ -97,6 +96,9 @@ class MenuConfig {
} }
abstract class MenuEntryBase<T> { abstract class MenuEntryBase<T> {
bool dismissOnClicked;
MenuEntryBase({this.dismissOnClicked = false});
List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf); List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf);
} }
@ -112,9 +114,19 @@ class MenuEntryDivider<T> extends MenuEntryBase<T> {
} }
} }
typedef RadioOptionsGetter = List<Tuple2<String, String>> Function(); class MenuEntryRadioOption {
String text;
String value;
bool dismissOnClicked;
MenuEntryRadioOption(
{required this.text, required this.value, this.dismissOnClicked = false});
}
typedef RadioOptionsGetter = List<MenuEntryRadioOption> Function();
typedef RadioCurOptionGetter = Future<String> Function(); typedef RadioCurOptionGetter = Future<String> Function();
typedef RadioOptionSetter = Future<void> Function(String); typedef RadioOptionSetter = Future<void> Function(
String oldValue, String newValue);
class MenuEntryRadioUtils<T> {} class MenuEntryRadioUtils<T> {}
@ -129,24 +141,28 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
{required this.text, {required this.text,
required this.optionsGetter, required this.optionsGetter,
required this.curOptionGetter, required this.curOptionGetter,
required this.optionSetter}) { required this.optionSetter,
dismissOnClicked = false})
: super(dismissOnClicked: dismissOnClicked) {
() async { () async {
_curOption.value = await curOptionGetter(); _curOption.value = await curOptionGetter();
}(); }();
} }
List<Tuple2<String, String>> get options => optionsGetter(); List<MenuEntryRadioOption> get options => optionsGetter();
RxString get curOption => _curOption; RxString get curOption => _curOption;
setOption(String option) async { setOption(String option) async {
await optionSetter(option); await optionSetter(_curOption.value, option);
final opt = await curOptionGetter(); if (_curOption.value != option) {
if (_curOption.value != opt) { final opt = await curOptionGetter();
_curOption.value = opt; if (_curOption.value != opt) {
_curOption.value = opt;
}
} }
} }
mod_menu.PopupMenuEntry<T> _buildMenuItem( mod_menu.PopupMenuEntry<T> _buildMenuItem(
BuildContext context, MenuConfig conf, Tuple2<String, String> opt) { BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) {
return mod_menu.PopupMenuItem( return mod_menu.PopupMenuItem(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
height: conf.height, height: conf.height,
@ -157,7 +173,7 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
child: Row( child: Row(
children: [ children: [
Text( Text(
opt.item1, opt.text,
style: const TextStyle( style: const TextStyle(
color: Colors.black, color: Colors.black,
fontSize: MenuConfig.fontSize, fontSize: MenuConfig.fontSize,
@ -169,7 +185,7 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
child: SizedBox( child: SizedBox(
width: 20.0, width: 20.0,
height: 20.0, height: 20.0,
child: Obx(() => opt.item2 == curOption.value child: Obx(() => opt.value == curOption.value
? Icon( ? Icon(
Icons.check, Icons.check,
color: conf.commonColor, color: conf.commonColor,
@ -180,9 +196,10 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
), ),
), ),
onPressed: () { onPressed: () {
if (opt.item2 != curOption.value) { if (opt.dismissOnClicked && Navigator.canPop(context)) {
setOption(opt.item2); Navigator.pop(context);
} }
setOption(opt.value);
}, },
), ),
); );
@ -206,24 +223,28 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
{required this.text, {required this.text,
required this.optionsGetter, required this.optionsGetter,
required this.curOptionGetter, required this.curOptionGetter,
required this.optionSetter}) { required this.optionSetter,
dismissOnClicked = false})
: super(dismissOnClicked: dismissOnClicked) {
() async { () async {
_curOption.value = await curOptionGetter(); _curOption.value = await curOptionGetter();
}(); }();
} }
List<Tuple2<String, String>> get options => optionsGetter(); List<MenuEntryRadioOption> get options => optionsGetter();
RxString get curOption => _curOption; RxString get curOption => _curOption;
setOption(String option) async { setOption(String option) async {
await optionSetter(option); await optionSetter(_curOption.value, option);
final opt = await curOptionGetter(); if (_curOption.value != option) {
if (_curOption.value != opt) { final opt = await curOptionGetter();
_curOption.value = opt; if (_curOption.value != opt) {
_curOption.value = opt;
}
} }
} }
mod_menu.PopupMenuEntry<T> _buildSecondMenu( mod_menu.PopupMenuEntry<T> _buildSecondMenu(
BuildContext context, MenuConfig conf, Tuple2<String, String> opt) { BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) {
return mod_menu.PopupMenuItem( return mod_menu.PopupMenuItem(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
height: conf.height, height: conf.height,
@ -234,7 +255,7 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
child: Row( child: Row(
children: [ children: [
Text( Text(
opt.item1, opt.text,
style: const TextStyle( style: const TextStyle(
color: Colors.black, color: Colors.black,
fontSize: MenuConfig.fontSize, fontSize: MenuConfig.fontSize,
@ -246,7 +267,7 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
child: SizedBox( child: SizedBox(
width: 20.0, width: 20.0,
height: 20.0, height: 20.0,
child: Obx(() => opt.item2 == curOption.value child: Obx(() => opt.value == curOption.value
? Icon( ? Icon(
Icons.check, Icons.check,
color: conf.commonColor, color: conf.commonColor,
@ -257,9 +278,10 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
), ),
), ),
onPressed: () { onPressed: () {
if (opt.item2 != curOption.value) { if (opt.dismissOnClicked && Navigator.canPop(context)) {
setOption(opt.item2); Navigator.pop(context);
} }
setOption(opt.value);
}, },
), ),
); );
@ -303,7 +325,8 @@ typedef SwitchSetter = Future<void> Function(bool);
abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> { abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
final String text; final String text;
MenuEntrySwitchBase({required this.text}); MenuEntrySwitchBase({required this.text, required dismissOnClicked})
: super(dismissOnClicked: dismissOnClicked);
RxBool get curOption; RxBool get curOption;
Future<void> setOption(bool option); Future<void> setOption(bool option);
@ -315,29 +338,40 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
mod_menu.PopupMenuItem( mod_menu.PopupMenuItem(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
height: conf.height, height: conf.height,
child: Obx( child: TextButton(
() => SwitchListTile( child: Container(
value: curOption.value, alignment: AlignmentDirectional.centerStart,
onChanged: (v) { height: conf.height,
setOption(v); child: Row(children: [
}, // const SizedBox(width: MenuConfig.midPadding),
title: Container( Text(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.height),
child: Text(
text, text,
style: const TextStyle( style: const TextStyle(
color: Colors.black, color: Colors.black,
fontSize: MenuConfig.fontSize, fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal), fontWeight: FontWeight.normal),
)), ),
dense: true, Expanded(
visualDensity: const VisualDensity( child: Align(
horizontal: VisualDensity.minimumDensity, alignment: Alignment.centerRight,
vertical: VisualDensity.minimumDensity, child: Obx(() => Switch(
), value: curOption.value,
contentPadding: const EdgeInsets.only(left: 8.0), onChanged: (v) {
), if (super.dismissOnClicked &&
Navigator.canPop(context)) {
Navigator.pop(context);
}
setOption(v);
},
)),
))
])),
onPressed: () {
if (super.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
}
setOption(!curOption.value);
},
), ),
) )
]; ];
@ -350,8 +384,11 @@ class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
final RxBool _curOption = false.obs; final RxBool _curOption = false.obs;
MenuEntrySwitch( MenuEntrySwitch(
{required String text, required this.getter, required this.setter}) {required String text,
: super(text: text) { required this.getter,
required this.setter,
dismissOnClicked = false})
: super(text: text, dismissOnClicked: dismissOnClicked) {
() async { () async {
_curOption.value = await getter(); _curOption.value = await getter();
}(); }();
@ -377,8 +414,11 @@ class MenuEntrySwitch2<T> extends MenuEntrySwitchBase<T> {
final SwitchSetter setter; final SwitchSetter setter;
MenuEntrySwitch2( MenuEntrySwitch2(
{required String text, required this.getter, required this.setter}) {required String text,
: super(text: text); required this.getter,
required this.setter,
dismissOnClicked = false})
: super(text: text, dismissOnClicked: dismissOnClicked);
@override @override
RxBool get curOption => getter(); RxBool get curOption => getter();
@ -392,10 +432,7 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
final String text; final String text;
final List<MenuEntryBase<T>> entries; final List<MenuEntryBase<T>> entries;
MenuEntrySubMenu({ MenuEntrySubMenu({required this.text, required this.entries});
required this.text,
required this.entries,
});
@override @override
List<mod_menu.PopupMenuEntry<T>> build( List<mod_menu.PopupMenuEntry<T>> build(
@ -436,10 +473,11 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
final Widget Function(TextStyle? style) childBuilder; final Widget Function(TextStyle? style) childBuilder;
Function() proc; Function() proc;
MenuEntryButton({ MenuEntryButton(
required this.childBuilder, {required this.childBuilder,
required this.proc, required this.proc,
}); dismissOnClicked = false})
: super(dismissOnClicked: dismissOnClicked);
@override @override
List<mod_menu.PopupMenuEntry<T>> build( List<mod_menu.PopupMenuEntry<T>> build(
@ -459,6 +497,9 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
fontWeight: FontWeight.normal), fontWeight: FontWeight.normal),
)), )),
onPressed: () { onPressed: () {
if (super.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
}
proc(); proc();
}, },
), ),

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/chat_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:tuple/tuple.dart'; import 'package:rxdart/rxdart.dart' as rxdart;
import '../../common.dart'; import '../../common.dart';
import '../../mobile/widgets/dialog.dart'; import '../../mobile/widgets/dialog.dart';
@ -16,7 +16,7 @@ import './material_mod_popup_menu.dart' as mod_menu;
class _MenubarTheme { class _MenubarTheme {
static const Color commonColor = MyTheme.accent; static const Color commonColor = MyTheme.accent;
// kMinInteractiveDimension // kMinInteractiveDimension
static const double height = 24.0; static const double height = 25.0;
static const double dividerHeight = 12.0; static const double dividerHeight = 12.0;
} }
@ -290,9 +290,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
style: style, style: style,
), ),
proc: () { proc: () {
Navigator.pop(context);
bind.sessionRefresh(id: widget.id); bind.sessionRefresh(id: widget.id);
}, },
dismissOnClicked: true,
)); ));
} }
displayMenu.add(MenuEntryButton<String>( displayMenu.add(MenuEntryButton<String>(
@ -301,9 +301,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
style: style, style: style,
), ),
proc: () { proc: () {
Navigator.pop(context);
showSetOSPassword(widget.id, false, widget.ffi.dialogManager); showSetOSPassword(widget.id, false, widget.ffi.dialogManager);
}, },
dismissOnClicked: true,
)); ));
if (!isWebDesktop) { if (!isWebDesktop) {
@ -314,7 +314,6 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
style: style, style: style,
), ),
proc: () { proc: () {
Navigator.pop(context);
() async { () async {
ClipboardData? data = ClipboardData? data =
await Clipboard.getData(Clipboard.kTextPlain); await Clipboard.getData(Clipboard.kTextPlain);
@ -323,6 +322,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
} }
}(); }();
}, },
dismissOnClicked: true,
)); ));
} }
@ -332,9 +332,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
style: style, style: style,
), ),
proc: () { proc: () {
Navigator.pop(context);
widget.ffi.cursorModel.reset(); widget.ffi.cursorModel.reset();
}, },
dismissOnClicked: true,
)); ));
} }
@ -346,9 +346,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
style: style, style: style,
), ),
proc: () { proc: () {
Navigator.pop(context);
bind.sessionCtrlAltDel(id: widget.id); bind.sessionCtrlAltDel(id: widget.id);
}, },
dismissOnClicked: true,
)); ));
} }
@ -358,9 +358,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
style: style, style: style,
), ),
proc: () { proc: () {
Navigator.pop(context);
bind.sessionLockScreen(id: widget.id); bind.sessionLockScreen(id: widget.id);
}, },
dismissOnClicked: true,
)); ));
if (pi.platform == 'Windows') { if (pi.platform == 'Windows') {
@ -371,13 +371,13 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
style: style, style: style,
)), )),
proc: () { proc: () {
Navigator.pop(context);
RxBool blockInput = BlockInputState.find(widget.id); RxBool blockInput = BlockInputState.find(widget.id);
bind.sessionToggleOption( bind.sessionToggleOption(
id: widget.id, id: widget.id,
value: '${blockInput.value ? "un" : ""}block-input'); value: '${blockInput.value ? "un" : ""}block-input');
blockInput.value = !blockInput.value; blockInput.value = !blockInput.value;
}, },
dismissOnClicked: true,
)); ));
} }
} }
@ -392,9 +392,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
style: style, style: style,
), ),
proc: () { proc: () {
Navigator.pop(context);
showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager);
}, },
dismissOnClicked: true,
)); ));
} }
@ -406,45 +406,54 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
MenuEntryRadios<String>( MenuEntryRadios<String>(
text: translate('Ratio'), text: translate('Ratio'),
optionsGetter: () => [ optionsGetter: () => [
Tuple2<String, String>(translate('Original'), 'original'), MenuEntryRadioOption(
Tuple2<String, String>(translate('Shrink'), 'shrink'), text: translate('Scale original'), value: 'original'),
Tuple2<String, String>(translate('Stretch'), 'stretch'), MenuEntryRadioOption(
text: translate('Scale adaptive'), value: 'adaptive'),
], ],
curOptionGetter: () async { curOptionGetter: () async {
return await bind.sessionGetOption( return await bind.sessionGetOption(
id: widget.id, arg: 'view-style') ?? id: widget.id, arg: 'view-style') ??
''; 'adaptive';
}, },
optionSetter: (String v) async { optionSetter: (String oldValue, String newValue) async {
await bind.sessionPeerOption( await bind.sessionPeerOption(
id: widget.id, name: "view-style", value: v); id: widget.id, name: "view-style", value: newValue);
widget.ffi.canvasModel.updateViewStyle(); widget.ffi.canvasModel.updateViewStyle();
}), }),
MenuEntryDivider<String>(), MenuEntryDivider<String>(),
MenuEntryRadios<String>( MenuEntryRadios<String>(
text: translate('Scroll Style'), text: translate('Scroll Style'),
optionsGetter: () => [ optionsGetter: () => [
Tuple2<String, String>(translate('ScrollAuto'), 'scrollauto'), MenuEntryRadioOption(
Tuple2<String, String>(translate('Scrollbar'), 'scrollbar'), text: translate('ScrollAuto'), value: 'scrollauto'),
MenuEntryRadioOption(
text: translate('Scrollbar'), value: 'scrollbar'),
], ],
curOptionGetter: () async { curOptionGetter: () async {
return await bind.sessionGetOption( return await bind.sessionGetOption(
id: widget.id, arg: 'scroll-style') ?? id: widget.id, arg: 'scroll-style') ??
''; '';
}, },
optionSetter: (String v) async { optionSetter: (String oldValue, String newValue) async {
await bind.sessionPeerOption( await bind.sessionPeerOption(
id: widget.id, name: "scroll-style", value: v); id: widget.id, name: "scroll-style", value: newValue);
widget.ffi.canvasModel.updateScrollStyle(); widget.ffi.canvasModel.updateScrollStyle();
}), }),
MenuEntryDivider<String>(), MenuEntryDivider<String>(),
MenuEntryRadios<String>( MenuEntryRadios<String>(
text: translate('Image Quality'), text: translate('Image Quality'),
optionsGetter: () => [ optionsGetter: () => [
Tuple2<String, String>(translate('Good image quality'), 'best'), MenuEntryRadioOption(
Tuple2<String, String>(translate('Balanced'), 'balanced'), text: translate('Good image quality'), value: 'best'),
Tuple2<String, String>( MenuEntryRadioOption(
translate('Optimize reaction time'), 'low'), text: translate('Balanced'), value: 'balanced'),
MenuEntryRadioOption(
text: translate('Optimize reaction time'), value: 'low'),
MenuEntryRadioOption(
text: translate('Custom'),
value: 'custom',
dismissOnClicked: true),
], ],
curOptionGetter: () async { curOptionGetter: () async {
String quality = String quality =
@ -452,8 +461,46 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
if (quality == '') quality = 'balanced'; if (quality == '') quality = 'balanced';
return quality; return quality;
}, },
optionSetter: (String v) async { optionSetter: (String oldValue, String newValue) async {
await bind.sessionSetImageQuality(id: widget.id, value: v); if (oldValue != newValue) {
await bind.sessionSetImageQuality(id: widget.id, value: newValue);
}
if (newValue == 'custom') {
final btnCancel = msgBoxButton(translate('Close'), () {
widget.ffi.dialogManager.dismissAll();
});
final quality =
await bind.sessionGetCustomImageQuality(id: widget.id);
final double initValue = quality != null && quality.isNotEmpty
? quality[0].toDouble()
: 50.0;
final RxDouble sliderValue = RxDouble(initValue);
final rxReplay = rxdart.ReplaySubject<double>();
rxReplay
.throttleTime(const Duration(milliseconds: 1000),
trailing: true, leading: false)
.listen((double v) {
() async {
await bind.sessionSetCustomImageQuality(
id: widget.id, value: v.toInt());
}();
});
final slider = Obx(() {
return Slider(
value: sliderValue.value,
max: 100,
divisions: 100,
label: sliderValue.value.round().toString(),
onChanged: (double value) {
sliderValue.value = value;
rxReplay.add(value);
},
);
});
msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality',
slider, [btnCancel]);
}
}), }),
MenuEntryDivider<String>(), MenuEntryDivider<String>(),
MenuEntrySwitch<String>( MenuEntrySwitch<String>(

View File

@ -7,6 +7,7 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/generated_bridge.dart'; import 'package:flutter_hbb/generated_bridge.dart';
import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/chat_model.dart';
@ -300,6 +301,9 @@ class FfiModel with ChangeNotifier {
/// Handle the peer info event based on [evt]. /// Handle the peer info event based on [evt].
void handlePeerInfo(Map<String, dynamic> evt, String peerId) async { void handlePeerInfo(Map<String, dynamic> evt, String peerId) async {
// recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
bind.mainLoadRecentPeers();
parent.target?.dialogManager.dismissAll(); parent.target?.dialogManager.dismissAll();
_pi.version = evt['version']; _pi.version = evt['version'];
_pi.username = evt['username']; _pi.username = evt['username'];
@ -497,39 +501,11 @@ class CanvasModel with ChangeNotifier {
return; return;
} }
final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720); _scale = 1.0;
final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280); if (style == 'adaptive') {
final s1 = size.width / getDisplayWidth();
// Closure to perform shrink operation. final s2 = size.height / getDisplayHeight();
final shrinkOp = () { _scale = s1 < s2 ? s1 : s2;
final s = s1 < s2 ? s1 : s2;
if (s < 1) {
_scale = s;
}
};
// Closure to perform stretch operation.
final stretchOp = () {
final s = s1 < s2 ? s1 : s2;
if (s > 1) {
_scale = s;
}
};
// Closure to perform default operation(set the scale to 1.0).
final defaultOp = () {
_scale = 1.0;
};
// // On desktop, shrink is the default behavior.
// if (isDesktop) {
// shrinkOp();
// } else {
defaultOp();
// }
if (style == 'shrink') {
shrinkOp();
} else if (style == 'stretch') {
stretchOp();
} }
_x = (size.width - getDisplayWidth() * _scale) / 2; _x = (size.width - getDisplayWidth() * _scale) / 2;
@ -557,11 +533,17 @@ class CanvasModel with ChangeNotifier {
} }
int getDisplayWidth() { int getDisplayWidth() {
return parent.target?.ffiModel.display.width ?? 1080; final defaultWidth = (isDesktop || isWebDesktop)
? kDesktopDefaultDisplayWidth
: kMobileDefaultDisplayWidth;
return parent.target?.ffiModel.display.width ?? defaultWidth;
} }
int getDisplayHeight() { int getDisplayHeight() {
return parent.target?.ffiModel.display.height ?? 720; final defaultHeight = (isDesktop || isWebDesktop)
? kDesktopDefaultDisplayHeight
: kMobileDefaultDisplayHeight;
return parent.target?.ffiModel.display.height ?? defaultHeight;
} }
Size get size { Size get size {
@ -577,9 +559,19 @@ class CanvasModel with ChangeNotifier {
var dxOffset = 0; var dxOffset = 0;
var dyOffset = 0; var dyOffset = 0;
if (dw > size.width) { if (dw > size.width) {
final X_debugNanOrInfinite = x - dw * (x / size.width) - _x;
if (X_debugNanOrInfinite.isInfinite || X_debugNanOrInfinite.isNaN) {
debugPrint(
'REMOVE ME ============================ X_debugNanOrInfinite $x,$dw,$_scale,${size.width},$_x');
}
dxOffset = (x - dw * (x / size.width) - _x).toInt(); dxOffset = (x - dw * (x / size.width) - _x).toInt();
} }
if (dh > size.height) { if (dh > size.height) {
final Y_debugNanOrInfinite = y - dh * (y / size.height) - _y;
if (Y_debugNanOrInfinite.isInfinite || Y_debugNanOrInfinite.isNaN) {
debugPrint(
'REMOVE ME ============================ Y_debugNanOrInfinite $y,$dh,$_scale,${size.height},$_y');
}
dyOffset = (y - dh * (y / size.height) - _y).toInt(); dyOffset = (y - dh * (y / size.height) - _y).toInt();
} }
_x += dxOffset; _x += dxOffset;
@ -947,16 +939,16 @@ class FFI {
late final QualityMonitorModel qualityMonitorModel; // session late final QualityMonitorModel qualityMonitorModel; // session
FFI() { FFI() {
this.imageModel = ImageModel(WeakReference(this)); imageModel = ImageModel(WeakReference(this));
this.ffiModel = FfiModel(WeakReference(this)); ffiModel = FfiModel(WeakReference(this));
this.cursorModel = CursorModel(WeakReference(this)); cursorModel = CursorModel(WeakReference(this));
this.canvasModel = CanvasModel(WeakReference(this)); canvasModel = CanvasModel(WeakReference(this));
this.serverModel = ServerModel(WeakReference(this)); // use global FFI serverModel = ServerModel(WeakReference(this)); // use global FFI
this.chatModel = ChatModel(WeakReference(this)); chatModel = ChatModel(WeakReference(this));
this.fileModel = FileModel(WeakReference(this)); fileModel = FileModel(WeakReference(this));
this.abModel = AbModel(WeakReference(this)); abModel = AbModel(WeakReference(this));
this.userModel = UserModel(WeakReference(this)); userModel = UserModel(WeakReference(this));
this.qualityMonitorModel = QualityMonitorModel(WeakReference(this)); qualityMonitorModel = QualityMonitorModel(WeakReference(this));
} }
/// Send a mouse tap event(down and up). /// Send a mouse tap event(down and up).
@ -1260,20 +1252,20 @@ class PeerInfo {
Future<void> savePreference(String id, double xCursor, double yCursor, Future<void> savePreference(String id, double xCursor, double yCursor,
double xCanvas, double yCanvas, double scale, int currentDisplay) async { double xCanvas, double yCanvas, double scale, int currentDisplay) async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
final p = Map<String, dynamic>(); final p = <String, dynamic>{};
p['xCursor'] = xCursor; p['xCursor'] = xCursor;
p['yCursor'] = yCursor; p['yCursor'] = yCursor;
p['xCanvas'] = xCanvas; p['xCanvas'] = xCanvas;
p['yCanvas'] = yCanvas; p['yCanvas'] = yCanvas;
p['scale'] = scale; p['scale'] = scale;
p['currentDisplay'] = currentDisplay; p['currentDisplay'] = currentDisplay;
prefs.setString('peer' + id, json.encode(p)); prefs.setString('peer$id', json.encode(p));
} }
Future<Map<String, dynamic>?> getPreference(String id) async { Future<Map<String, dynamic>?> getPreference(String id) async {
if (!isWebDesktop) return null; if (!isWebDesktop) return null;
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
var p = prefs.getString('peer' + id); var p = prefs.getString('peer$id');
if (p == null) return null; if (p == null) return null;
Map<String, dynamic> m = json.decode(p); Map<String, dynamic> m = json.decode(p);
return m; return m;
@ -1281,7 +1273,7 @@ Future<Map<String, dynamic>?> getPreference(String id) async {
void removePreference(String id) async { void removePreference(String id) async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.remove('peer' + id); prefs.remove('peer$id');
} }
void initializeCursorAndCanvas(FFI ffi) async { void initializeCursorAndCanvas(FFI ffi) async {

View File

@ -30,7 +30,7 @@ class PlatformFFI {
String _dir = ''; String _dir = '';
String _homeDir = ''; String _homeDir = '';
F2? _translate; F2? _translate;
var _eventHandlers = Map<String, Map<String, HandleEvent>>(); final _eventHandlers = Map<String, Map<String, HandleEvent>>();
late RustdeskImpl _ffiBind; late RustdeskImpl _ffiBind;
late String _appType; late String _appType;
void Function(Map<String, dynamic>)? _eventCallback; void Function(Map<String, dynamic>)? _eventCallback;
@ -50,27 +50,27 @@ class PlatformFFI {
} }
bool registerEventHandler( bool registerEventHandler(
String event_name, String handler_name, HandleEvent handler) { String eventName, String handlerName, HandleEvent handler) {
debugPrint('registerEventHandler $event_name $handler_name'); debugPrint('registerEventHandler $eventName $handlerName');
var handlers = _eventHandlers[event_name]; var handlers = _eventHandlers[eventName];
if (handlers == null) { if (handlers == null) {
_eventHandlers[event_name] = {handler_name: handler}; _eventHandlers[eventName] = {handlerName: handler};
return true; return true;
} else { } else {
if (handlers.containsKey(handler_name)) { if (handlers.containsKey(handlerName)) {
return false; return false;
} else { } else {
handlers[handler_name] = handler; handlers[handlerName] = handler;
return true; return true;
} }
} }
} }
void unregisterEventHandler(String event_name, String handler_name) { void unregisterEventHandler(String eventName, String handlerName) {
debugPrint('unregisterEventHandler $event_name $handler_name'); debugPrint('unregisterEventHandler $eventName $handlerName');
var handlers = _eventHandlers[event_name]; var handlers = _eventHandlers[eventName];
if (handlers != null) { if (handlers != null) {
handlers.remove(handler_name); handlers.remove(handlerName);
} }
} }

View File

@ -10,9 +10,8 @@ class Peer {
final List<dynamic> tags; final List<dynamic> tags;
bool online = false; bool online = false;
Peer.fromJson(String id, Map<String, dynamic> json) Peer.fromJson(this.id, Map<String, dynamic> json)
: id = id, : username = json['username'] ?? '',
username = json['username'] ?? '',
hostname = json['hostname'] ?? '', hostname = json['hostname'] ?? '',
platform = json['platform'] ?? '', platform = json['platform'] ?? '',
tags = json['tags'] ?? []; tags = json['tags'] ?? [];
@ -35,57 +34,52 @@ class Peer {
} }
class Peers extends ChangeNotifier { class Peers extends ChangeNotifier {
late String _name; final String name;
late List<Peer> _peers; final String loadEvent;
late final _loadEvent; List<Peer> peers;
static const _cbQueryOnlines = 'callback_query_onlines'; static const _cbQueryOnlines = 'callback_query_onlines';
Peers(String name, String loadEvent, List<Peer> _initPeers) { Peers({required this.name, required this.peers, required this.loadEvent}) {
_name = name; platformFFI.registerEventHandler(_cbQueryOnlines, name, (evt) {
_loadEvent = loadEvent;
_peers = _initPeers;
platformFFI.registerEventHandler(_cbQueryOnlines, _name, (evt) {
_updateOnlineState(evt); _updateOnlineState(evt);
}); });
platformFFI.registerEventHandler(_loadEvent, _name, (evt) { platformFFI.registerEventHandler(loadEvent, name, (evt) {
_updatePeers(evt); _updatePeers(evt);
}); });
} }
List<Peer> get peers => _peers;
@override @override
void dispose() { void dispose() {
platformFFI.unregisterEventHandler(_cbQueryOnlines, _name); platformFFI.unregisterEventHandler(_cbQueryOnlines, name);
platformFFI.unregisterEventHandler(_loadEvent, _name); platformFFI.unregisterEventHandler(loadEvent, name);
super.dispose(); super.dispose();
} }
Peer getByIndex(int index) { Peer getByIndex(int index) {
if (index < _peers.length) { if (index < peers.length) {
return _peers[index]; return peers[index];
} else { } else {
return Peer.loading(); return Peer.loading();
} }
} }
int getPeersCount() { int getPeersCount() {
return _peers.length; return peers.length;
} }
void _updateOnlineState(Map<String, dynamic> evt) { void _updateOnlineState(Map<String, dynamic> evt) {
evt['onlines'].split(',').forEach((online) { evt['onlines'].split(',').forEach((online) {
for (var i = 0; i < _peers.length; i++) { for (var i = 0; i < peers.length; i++) {
if (_peers[i].id == online) { if (peers[i].id == online) {
_peers[i].online = true; peers[i].online = true;
} }
} }
}); });
evt['offlines'].split(',').forEach((offline) { evt['offlines'].split(',').forEach((offline) {
for (var i = 0; i < _peers.length; i++) { for (var i = 0; i < peers.length; i++) {
if (_peers[i].id == offline) { if (peers[i].id == offline) {
_peers[i].online = false; peers[i].online = false;
} }
} }
}); });
@ -95,19 +89,19 @@ class Peers extends ChangeNotifier {
void _updatePeers(Map<String, dynamic> evt) { void _updatePeers(Map<String, dynamic> evt) {
final onlineStates = _getOnlineStates(); final onlineStates = _getOnlineStates();
_peers = _decodePeers(evt['peers']); peers = _decodePeers(evt['peers']);
_peers.forEach((peer) { for (var peer in peers) {
final state = onlineStates[peer.id]; final state = onlineStates[peer.id];
peer.online = state != null && state != false; peer.online = state != null && state != false;
}); }
notifyListeners(); notifyListeners();
} }
Map<String, bool> _getOnlineStates() { Map<String, bool> _getOnlineStates() {
var onlineStates = new Map<String, bool>(); var onlineStates = <String, bool>{};
_peers.forEach((peer) { for (var peer in peers) {
onlineStates[peer.id] = peer.online; onlineStates[peer.id] = peer.online;
}); }
return onlineStates; return onlineStates;
} }
@ -121,7 +115,7 @@ class Peers extends ChangeNotifier {
Peer.fromJson(s[0] as String, s[1] as Map<String, dynamic>)) Peer.fromJson(s[0] as String, s[1] as Map<String, dynamic>))
.toList(); .toList();
} catch (e) { } catch (e) {
print('peers(): $e'); debugPrint('peers(): $e');
} }
return []; return [];
} }

View File

@ -1,5 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:ui';
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -37,9 +36,9 @@ class RustDeskMultiWindowManager {
int? _fileTransferWindowId; int? _fileTransferWindowId;
int? _portForwardWindowId; int? _portForwardWindowId;
Future<dynamic> new_remote_desktop(String remote_id) async { Future<dynamic> newRemoteDesktop(String remoteId) async {
final msg = final msg =
jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remote_id}); jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remoteId});
try { try {
final ids = await DesktopMultiWindow.getAllSubWindowIds(); final ids = await DesktopMultiWindow.getAllSubWindowIds();
@ -63,9 +62,9 @@ class RustDeskMultiWindowManager {
} }
} }
Future<dynamic> new_file_transfer(String remote_id) async { Future<dynamic> newFileTransfer(String remoteId) async {
final msg = final msg =
jsonEncode({"type": WindowType.FileTransfer.index, "id": remote_id}); jsonEncode({"type": WindowType.FileTransfer.index, "id": remoteId});
try { try {
final ids = await DesktopMultiWindow.getAllSubWindowIds(); final ids = await DesktopMultiWindow.getAllSubWindowIds();
@ -88,12 +87,9 @@ class RustDeskMultiWindowManager {
} }
} }
Future<dynamic> new_port_forward(String remote_id, bool isRDP) async { Future<dynamic> newPortForward(String remoteId, bool isRDP) async {
final msg = jsonEncode({ final msg = jsonEncode(
"type": WindowType.PortForward.index, {"type": WindowType.PortForward.index, "id": remoteId, "isRDP": isRDP});
"id": remote_id,
"isRDP": isRDP
});
try { try {
final ids = await DesktopMultiWindow.getAllSubWindowIds(); final ids = await DesktopMultiWindow.getAllSubWindowIds();

View File

@ -73,6 +73,7 @@ dependencies:
contextmenu: ^3.0.0 contextmenu: ^3.0.0
desktop_drop: ^0.3.3 desktop_drop: ^0.3.3
scroll_pos: ^0.3.0 scroll_pos: ^0.3.0
rxdart: ^0.27.5
dev_dependencies: dev_dependencies:
flutter_launcher_icons: ^0.9.1 flutter_launcher_icons: ^0.9.1

View File

@ -1278,11 +1278,11 @@ impl LoginConfigHandler {
/// ///
/// * `username` - The name of the peer. /// * `username` - The name of the peer.
/// * `pi` - The peer info. /// * `pi` - The peer info.
pub fn handle_peer_info(&mut self, pi: PeerInfo) { pub fn handle_peer_info(&mut self, pi: &PeerInfo) {
if !pi.version.is_empty() { if !pi.version.is_empty() {
self.version = hbb_common::get_version_number(&pi.version); self.version = hbb_common::get_version_number(&pi.version);
} }
self.features = pi.features.into_option(); self.features = pi.features.clone().into_option();
let serde = PeerInfoSerde { let serde = PeerInfoSerde {
username: pi.username.clone(), username: pi.username.clone(),
hostname: pi.hostname.clone(), hostname: pi.hostname.clone(),

View File

@ -143,14 +143,6 @@ pub fn session_get_toggle_option_sync(id: String, arg: String) -> SyncReturn<boo
SyncReturn(res) SyncReturn(res)
} }
pub fn session_get_image_quality(id: String) -> Option<String> {
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
Some(session.get_image_quality())
} else {
None
}
}
pub fn session_get_option(id: String, arg: String) -> Option<String> { pub fn session_get_option(id: String, arg: String) -> Option<String> {
if let Some(session) = SESSIONS.read().unwrap().get(&id) { if let Some(session) = SESSIONS.read().unwrap().get(&id) {
Some(session.get_option(arg)) Some(session.get_option(arg))
@ -190,12 +182,34 @@ pub fn session_toggle_option(id: String, value: String) {
} }
} }
pub fn session_get_image_quality(id: String) -> Option<String> {
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
Some(session.get_image_quality())
} else {
None
}
}
pub fn session_set_image_quality(id: String, value: String) { pub fn session_set_image_quality(id: String, value: String) {
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
session.save_image_quality(value); session.save_image_quality(value);
} }
} }
pub fn session_get_custom_image_quality(id: String) -> Option<Vec<i32>> {
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
Some(session.get_custom_image_quality())
} else {
None
}
}
pub fn session_set_custom_image_quality(id: String, value: i32) {
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
session.save_custom_image_quality(value);
}
}
pub fn session_lock_screen(id: String) { pub fn session_lock_screen(id: String) {
if let Some(session) = SESSIONS.read().unwrap().get(&id) { if let Some(session) = SESSIONS.read().unwrap().get(&id) {
session.lock_screen(); session.lock_screen();

View File

@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "中继连接"), ("Relay Connection", "中继连接"),
("Secure Connection", "安全连接"), ("Secure Connection", "安全连接"),
("Insecure Connection", "非安全连接"), ("Insecure Connection", "非安全连接"),
("Scale original", "原始尺寸"),
("Scale adaptive", "适应窗口"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Připojení relé"), ("Relay Connection", "Připojení relé"),
("Secure Connection", "Zabezpečené připojení"), ("Secure Connection", "Zabezpečené připojení"),
("Insecure Connection", "Nezabezpečené připojení"), ("Insecure Connection", "Nezabezpečené připojení"),
("Scale original", "Měřítko původní"),
("Scale adaptive", "Měřítko adaptivní"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Relæforbindelse"), ("Relay Connection", "Relæforbindelse"),
("Secure Connection", "Sikker forbindelse"), ("Secure Connection", "Sikker forbindelse"),
("Insecure Connection", "Usikker forbindelse"), ("Insecure Connection", "Usikker forbindelse"),
("Scale original", "Skala original"),
("Scale adaptive", "Skala adaptiv"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Relaisverbindung"), ("Relay Connection", "Relaisverbindung"),
("Secure Connection", "Sichere Verbindung"), ("Secure Connection", "Sichere Verbindung"),
("Insecure Connection", "Unsichere Verbindung"), ("Insecure Connection", "Unsichere Verbindung"),
("Scale original", "Original skalieren"),
("Scale adaptive", "Adaptiv skalieren"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Relajsa Konekto"), ("Relay Connection", "Relajsa Konekto"),
("Secure Connection", "Sekura Konekto"), ("Secure Connection", "Sekura Konekto"),
("Insecure Connection", "Nesekura Konekto"), ("Insecure Connection", "Nesekura Konekto"),
("Scale original", "Skalo originalo"),
("Scale adaptive", "Skalo adapta"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Conexión de relé"), ("Relay Connection", "Conexión de relé"),
("Secure Connection", "Conexión segura"), ("Secure Connection", "Conexión segura"),
("Insecure Connection", "Conexión insegura"), ("Insecure Connection", "Conexión insegura"),
("Scale original", "escala originales"),
("Scale adaptive", "Adaptable a escala"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Connexion relais"), ("Relay Connection", "Connexion relais"),
("Secure Connection", "Connexion sécurisée"), ("Secure Connection", "Connexion sécurisée"),
("Insecure Connection", "Connexion non sécurisée"), ("Insecure Connection", "Connexion non sécurisée"),
("Scale original", "Échelle d'origine"),
("Scale adaptive", "Échelle adaptative"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Relé csatlakozás"), ("Relay Connection", "Relé csatlakozás"),
("Secure Connection", "Biztonságos kapcsolat"), ("Secure Connection", "Biztonságos kapcsolat"),
("Insecure Connection", "Nem biztonságos kapcsolat"), ("Insecure Connection", "Nem biztonságos kapcsolat"),
("Scale original", "Eredeti méretarány"),
("Scale adaptive", "Skála adaptív"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Koneksi Relay"), ("Relay Connection", "Koneksi Relay"),
("Secure Connection", "Koneksi aman"), ("Secure Connection", "Koneksi aman"),
("Insecure Connection", "Koneksi Tidak Aman"), ("Insecure Connection", "Koneksi Tidak Aman"),
("Scale original", "Skala asli"),
("Scale adaptive", "Skala adaptif"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -316,5 +316,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Collegamento a relè"), ("Relay Connection", "Collegamento a relè"),
("Secure Connection", "Connessione sicura"), ("Secure Connection", "Connessione sicura"),
("Insecure Connection", "Connessione insicura"), ("Insecure Connection", "Connessione insicura"),
("Scale original", "Scala originale"),
("Scale adaptive", "Scala adattiva"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -314,5 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "リレー接続"), ("Relay Connection", "リレー接続"),
("Secure Connection", "安全な接続"), ("Secure Connection", "安全な接続"),
("Insecure Connection", "安全でない接続"), ("Insecure Connection", "安全でない接続"),
("Scale original", "オリジナルサイズ"),
("Scale adaptive", "フィットウィンドウ"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -314,5 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "릴레이 연결"), ("Relay Connection", "릴레이 연결"),
("Secure Connection", "보안 연결"), ("Secure Connection", "보안 연결"),
("Insecure Connection", "안전하지 않은 연결"), ("Insecure Connection", "안전하지 않은 연결"),
("Scale original", "원래 크기"),
("Scale adaptive", "맞는 창"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -318,5 +318,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Połączenie przekaźnika"), ("Relay Connection", "Połączenie przekaźnika"),
("Secure Connection", "Bezpieczne połączenie"), ("Secure Connection", "Bezpieczne połączenie"),
("Insecure Connection", "Niepewne połączenie"), ("Insecure Connection", "Niepewne połączenie"),
("Scale original", "Skala oryginalna"),
("Scale adaptive", "Skala adaptacyjna"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -314,5 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Conexão de relé"), ("Relay Connection", "Conexão de relé"),
("Secure Connection", "Conexão segura"), ("Secure Connection", "Conexão segura"),
("Insecure Connection", "Conexão insegura"), ("Insecure Connection", "Conexão insegura"),
("Scale original", "Escala original"),
("Scale adaptive", "Escala adaptável"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", ""), ("Relay Connection", ""),
("Secure Connection", ""), ("Secure Connection", ""),
("Insecure Connection", ""), ("Insecure Connection", ""),
("Scale original", ""),
("Scale adaptive", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Релейное соединение"), ("Relay Connection", "Релейное соединение"),
("Secure Connection", "Безопасное соединение"), ("Secure Connection", "Безопасное соединение"),
("Insecure Connection", "Небезопасное соединение"), ("Insecure Connection", "Небезопасное соединение"),
("Scale original", "Оригинал масштаба"),
("Scale adaptive", "Масштаб адаптивный"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Reléové pripojenie"), ("Relay Connection", "Reléové pripojenie"),
("Secure Connection", "Zabezpečené pripojenie"), ("Secure Connection", "Zabezpečené pripojenie"),
("Insecure Connection", "Nezabezpečené pripojenie"), ("Insecure Connection", "Nezabezpečené pripojenie"),
("Scale original", "Pôvodná mierka"),
("Scale adaptive", "Prispôsobivá mierka"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", ""), ("Relay Connection", ""),
("Secure Connection", ""), ("Secure Connection", ""),
("Insecure Connection", ""), ("Insecure Connection", ""),
("Scale original", ""),
("Scale adaptive", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Röle Bağlantısı"), ("Relay Connection", "Röle Bağlantısı"),
("Secure Connection", "Güvenli bağlantı"), ("Secure Connection", "Güvenli bağlantı"),
("Insecure Connection", "Güvenli Bağlantı"), ("Insecure Connection", "Güvenli Bağlantı"),
("Scale original", "Orijinali ölçeklendir"),
("Scale adaptive", "Ölçek uyarlanabilir"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "中繼連接"), ("Relay Connection", "中繼連接"),
("Secure Connection", "安全連接"), ("Secure Connection", "安全連接"),
("Insecure Connection", "非安全連接"), ("Insecure Connection", "非安全連接"),
("Scale original", "原始尺寸"),
("Scale adaptive", "適應窗口"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Kết nối chuyển tiếp"), ("Relay Connection", "Kết nối chuyển tiếp"),
("Secure Connection", "Kết nối an toàn"), ("Secure Connection", "Kết nối an toàn"),
("Insecure Connection", "Kết nối không an toàn"), ("Insecure Connection", "Kết nối không an toàn"),
("Scale original", "Quy mô gốc"),
("Scale adaptive", "Quy mô thích ứng"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -47,6 +47,11 @@ impl<T: InvokeUi> Session<T> {
self.lc.read().unwrap().image_quality.clone() self.lc.read().unwrap().image_quality.clone()
} }
/// Get custom image quality.
pub fn get_custom_image_quality(&self) -> Vec<i32> {
self.lc.read().unwrap().custom_image_quality.clone()
}
pub fn save_view_style(&mut self, value: String) { pub fn save_view_style(&mut self, value: String) {
self.lc.write().unwrap().save_view_style(value); self.lc.write().unwrap().save_view_style(value);
} }
@ -634,7 +639,7 @@ impl<T: InvokeUi> Interface for Session<T> {
} }
} else if !self.is_port_forward() { } else if !self.is_port_forward() {
if pi.displays.is_empty() { if pi.displays.is_empty() {
self.lc.write().unwrap().handle_peer_info(pi); self.lc.write().unwrap().handle_peer_info(&pi);
self.update_privacy_mode(); self.update_privacy_mode();
self.msgbox("error", "Remote Error", "No Display"); self.msgbox("error", "Remote Error", "No Display");
return; return;
@ -647,9 +652,9 @@ impl<T: InvokeUi> Interface for Session<T> {
self.set_display(current.x, current.y, current.width, current.height); self.set_display(current.x, current.y, current.width, current.height);
} }
self.update_privacy_mode(); self.update_privacy_mode();
// Save recent peers, then push event to flutter. So flutter can refresh peer page.
self.lc.write().unwrap().handle_peer_info(&pi);
self.set_peer_info(&pi); self.set_peer_info(&pi);
self.lc.write().unwrap().handle_peer_info(pi);
if self.is_file_transfer() { if self.is_file_transfer() {
self.close_success(); self.close_success();
} else if !self.is_port_forward() { } else if !self.is_port_forward() {