Merge pull request #1468 from fufesou/flutter_desktop_new_remote_menu_3

Flutter desktop cursor & popup menu refactor
This commit is contained in:
RustDesk 2022-09-07 20:04:08 +08:00 committed by GitHub
commit 7d60992770
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 550 additions and 266 deletions

View File

@ -1,7 +1,8 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../consts.dart'; import '../consts.dart';
import '../models/platform_model.dart';
// TODO: A lot of dup code.
class PrivacyModeState { class PrivacyModeState {
static String tag(String id) => 'privacy_mode_$id'; static String tag(String id) => 'privacy_mode_$id';
@ -156,3 +157,25 @@ class KeyboardEnabledState {
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id)); static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
} }
class RemoteCursorMovedState {
static String tag(String id) => 'remote_cursor_moved_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
// Server side, default true
final RxBool state = false.obs;
Get.put(state, tag: key);
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
}
}
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}

View File

@ -22,10 +22,10 @@ import '../../models/platform_model.dart';
/// Connection page for connecting to a remote peer. /// Connection page for connecting to a remote peer.
class ConnectionPage extends StatefulWidget { class ConnectionPage extends StatefulWidget {
ConnectionPage({Key? key}) : super(key: key); const ConnectionPage({Key? key}) : super(key: key);
@override @override
_ConnectionPageState createState() => _ConnectionPageState(); State<ConnectionPage> createState() => _ConnectionPageState();
} }
/// State for the connection page. /// State for the connection page.
@ -101,7 +101,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
], ],
).marginSymmetric(horizontal: 22), ).marginSymmetric(horizontal: 22),
), ),
Divider(), const Divider(),
SizedBox(height: 50, child: Obx(() => buildStatus())) SizedBox(height: 50, child: Obx(() => buildStatus()))
.paddingSymmetric(horizontal: 12.0) .paddingSymmetric(horizontal: 12.0)
]), ]),

View File

@ -19,11 +19,18 @@ import 'package:tray_manager/tray_manager.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
class _MenubarTheme {
static const Color commonColor = MyTheme.accent;
// kMinInteractiveDimension
static const double height = 25.0;
static const double dividerHeight = 12.0;
}
class DesktopHomePage extends StatefulWidget { class DesktopHomePage extends StatefulWidget {
DesktopHomePage({Key? key}) : super(key: key); const DesktopHomePage({Key? key}) : super(key: key);
@override @override
State<StatefulWidget> createState() => _DesktopHomePageState(); State<DesktopHomePage> createState() => _DesktopHomePageState();
} }
const borderColor = Color(0xFF2F65BA); const borderColor = Color(0xFF2F65BA);
@ -109,7 +116,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( Container(
height: 25, height: 25,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -135,11 +142,11 @@ class _DesktopHomePageState extends State<DesktopHomePage>
child: TextFormField( child: TextFormField(
controller: model.serverId, controller: model.serverId,
readOnly: true, readOnly: true,
decoration: const InputDecoration( decoration: InputDecoration(
border: InputBorder.none, border: InputBorder.none,
contentPadding: EdgeInsets.only(bottom: 20), contentPadding: EdgeInsets.only(bottom: 20),
), ),
style: const TextStyle( style: TextStyle(
fontSize: 22, fontSize: 22,
), ),
), ),
@ -322,18 +329,19 @@ class _DesktopHomePageState extends State<DesktopHomePage>
onTap: () => bind.mainUpdateTemporaryPassword(), onTap: () => bind.mainUpdateTemporaryPassword(),
onHover: (value) => refreshHover.value = value, onHover: (value) => refreshHover.value = value,
), ),
FutureBuilder<Widget>( const _PasswordPopupMenu(),
future: buildPasswordPopupMenu(context), // FutureBuilder<Widget>(
builder: (context, snapshot) { // future: buildPasswordPopupMenu(context),
if (snapshot.hasError) { // builder: (context, snapshot) {
print("${snapshot.error}"); // if (snapshot.hasError) {
} // print("${snapshot.error}");
if (snapshot.hasData) { // }
return snapshot.data!; // if (snapshot.hasData) {
} else { // return snapshot.data!;
return Offstage(); // } else {
} // return Offstage();
}) // }
// })
], ],
), ),
], ],
@ -366,7 +374,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
), ),
], ],
), ),
onTap: () => gFFI.serverModel.verificationMethod = value, onTap: () => gFFI.serverModel.setVerificationMethod(value),
); );
final temporary_enabled = final temporary_enabled =
gFFI.serverModel.verificationMethod != kUsePermanentPassword; gFFI.serverModel.verificationMethod != kUsePermanentPassword;
@ -403,8 +411,11 @@ class _DesktopHomePageState extends State<DesktopHomePage>
onTap: () { onTap: () {
if (gFFI.serverModel.temporaryPasswordLength != if (gFFI.serverModel.temporaryPasswordLength !=
e) { e) {
gFFI.serverModel.temporaryPasswordLength = e; () async {
bind.mainUpdateTemporaryPassword(); await gFFI.serverModel
.setTemporaryPasswordLength(e);
await bind.mainUpdateTemporaryPassword();
}();
} }
}, },
)) ))
@ -1035,3 +1046,120 @@ void setPasswordDialog() async {
); );
}); });
} }
class _PasswordPopupMenu extends StatefulWidget {
const _PasswordPopupMenu({Key? key}) : super(key: key);
@override
State<_PasswordPopupMenu> createState() => _PasswordPopupMenuState();
}
class _PasswordPopupMenuState extends State<_PasswordPopupMenu> {
final RxBool _tempEnabled = true.obs;
final RxBool _permEnabled = true.obs;
List<MenuEntryBase<String>> _buildMenus() {
return <MenuEntryBase<String>>[
MenuEntryRadios<String>(
text: translate('Password type'),
optionsGetter: () => [
MenuEntryRadioOption(
text: translate('Use temporary password'),
value: kUseTemporaryPassword),
MenuEntryRadioOption(
text: translate('Use permanent password'),
value: kUsePermanentPassword),
MenuEntryRadioOption(
text: translate('Use both passwords'),
value: kUseBothPasswords),
],
curOptionGetter: () async {
return gFFI.serverModel.verificationMethod;
},
optionSetter: (String oldValue, String newValue) async {
await bind.mainSetOption(
key: "verification-method", value: newValue);
await gFFI.serverModel.updatePasswordModel();
setState(() {
_tempEnabled.value =
gFFI.serverModel.verificationMethod != kUsePermanentPassword;
_permEnabled.value =
gFFI.serverModel.verificationMethod != kUseTemporaryPassword;
});
}),
MenuEntryDivider(),
MenuEntryButton<String>(
enabled: _permEnabled,
childBuilder: (TextStyle? style) => Text(
translate('Set permanent password'),
style: style,
),
proc: () {
setPasswordDialog();
},
dismissOnClicked: true,
),
MenuEntrySubMenu(
enabled: _tempEnabled,
text: translate('Set temporary password length'),
entries: [
MenuEntryRadios<String>(
enabled: _tempEnabled,
text: translate(''),
optionsGetter: () => [
MenuEntryRadioOption(
text: translate('6'),
value: '6',
enabled: _tempEnabled,
),
MenuEntryRadioOption(
text: translate('8'),
value: '8',
enabled: _tempEnabled,
),
MenuEntryRadioOption(
text: translate('10'),
value: '10',
enabled: _tempEnabled,
),
],
curOptionGetter: () async {
return gFFI.serverModel.temporaryPasswordLength;
},
optionSetter: (String oldValue, String newValue) async {
if (oldValue != newValue) {
await gFFI.serverModel.setTemporaryPasswordLength(newValue);
await gFFI.serverModel.updatePasswordModel();
}
}),
])
];
}
@override
Widget build(BuildContext context) {
final editHover = false.obs;
return mod_menu.PopupMenuButton(
padding: EdgeInsets.zero,
onHover: (v) => editHover.value = v,
tooltip: translate(''),
position: mod_menu.PopupMenuPosition.overSide,
itemBuilder: (BuildContext context) => _buildMenus()
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: _MenubarTheme.commonColor,
height: _MenubarTheme.height,
dividerHeight: _MenubarTheme.dividerHeight,
)))
.expand((i) => i)
.toList(),
child: Obx(() => Icon(Icons.edit,
size: 22,
color: editHover.value
? MyTheme.color(context).text
: const Color(0xFFDDDDDD))
.marginOnly(bottom: 2)),
);
}
}

View File

@ -1,5 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -314,8 +313,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
translate("Use permanent password"), translate("Use permanent password"),
translate("Use both passwords"), translate("Use both passwords"),
]; ];
bool tmp_enabled = model.verificationMethod != kUsePermanentPassword; bool tmpEnabled = model.verificationMethod != kUsePermanentPassword;
bool perm_enabled = model.verificationMethod != kUseTemporaryPassword; bool permEnabled = model.verificationMethod != kUseTemporaryPassword;
String currentValue = values[keys.indexOf(model.verificationMethod)]; String currentValue = values[keys.indexOf(model.verificationMethod)];
List<Widget> radios = values List<Widget> radios = values
.map((value) => _Radio<String>( .map((value) => _Radio<String>(
@ -324,16 +323,24 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
groupValue: currentValue, groupValue: currentValue,
label: value, label: value,
onChanged: ((value) { onChanged: ((value) {
model.verificationMethod = keys[values.indexOf(value)]; () async {
await model
.setVerificationMethod(keys[values.indexOf(value)]);
await model.updatePasswordModel();
}();
}), }),
enabled: !locked, enabled: !locked,
)) ))
.toList(); .toList();
var onChanged = tmp_enabled && !locked var onChanged = tmpEnabled && !locked
? (value) { ? (value) {
if (value != null) if (value != null) {
model.temporaryPasswordLength = value.toString(); () async {
await model.setTemporaryPasswordLength(value.toString());
await model.updatePasswordModel();
}();
}
} }
: null; : null;
List<Widget> lengthRadios = ['6', '8', '10'] List<Widget> lengthRadios = ['6', '8', '10']
@ -365,10 +372,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
...lengthRadios, ...lengthRadios,
], ],
), ),
enabled: tmp_enabled && !locked), enabled: tmpEnabled && !locked),
radios[1], radios[1],
_SubButton('Set permanent password', setPasswordDialog, _SubButton('Set permanent password', setPasswordDialog,
perm_enabled && !locked), permEnabled && !locked),
radios[2], radios[2],
]); ]);
}))); })));

View File

@ -17,7 +17,7 @@ import '../../models/platform_model.dart';
enum LocationStatus { bread, textField } enum LocationStatus { bread, textField }
class FileManagerPage extends StatefulWidget { class FileManagerPage extends StatefulWidget {
FileManagerPage({Key? key, required this.id}) : super(key: key); const FileManagerPage({Key? key, required this.id}) : super(key: key);
final String id; final String id;
@override @override

View File

@ -21,8 +21,8 @@ class FileManagerTabPage extends StatefulWidget {
class _FileManagerTabPageState extends State<FileManagerTabPage> { class _FileManagerTabPageState extends State<FileManagerTabPage> {
DesktopTabController get tabController => Get.find<DesktopTabController>(); DesktopTabController get tabController => Get.find<DesktopTabController>();
static final IconData selectedIcon = Icons.file_copy_sharp; static const IconData selectedIcon = Icons.file_copy_sharp;
static final IconData unselectedIcon = Icons.file_copy_outlined; static const IconData unselectedIcon = Icons.file_copy_outlined;
_FileManagerTabPageState(Map<String, dynamic> params) { _FileManagerTabPageState(Map<String, dynamic> params) {
Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer)); Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer));

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
@ -8,6 +9,7 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:wakelock/wakelock.dart'; import 'package:wakelock/wakelock.dart';
import 'package:flutter_custom_cursor/flutter_custom_cursor.dart';
// import 'package:window_manager/window_manager.dart'; // import 'package:window_manager/window_manager.dart';
@ -22,7 +24,7 @@ import '../../common/shared_state.dart';
final initText = '\1' * 1024; final initText = '\1' * 1024;
class RemotePage extends StatefulWidget { class RemotePage extends StatefulWidget {
RemotePage({ const RemotePage({
Key? key, Key? key,
required this.id, required this.id,
required this.tabBarHeight, required this.tabBarHeight,
@ -32,7 +34,7 @@ class RemotePage extends StatefulWidget {
final double tabBarHeight; final double tabBarHeight;
@override @override
_RemotePageState createState() => _RemotePageState(); State<RemotePage> createState() => _RemotePageState();
} }
class _RemotePageState extends State<RemotePage> class _RemotePageState extends State<RemotePage>
@ -41,6 +43,7 @@ class _RemotePageState extends State<RemotePage>
String _value = ''; String _value = '';
final _cursorOverImage = false.obs; final _cursorOverImage = false.obs;
late RxBool _showRemoteCursor; late RxBool _showRemoteCursor;
late RxBool _remoteCursorMoved;
late RxBool _keyboardEnabled; late RxBool _keyboardEnabled;
final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _mobileFocusNode = FocusNode();
@ -60,8 +63,10 @@ class _RemotePageState extends State<RemotePage>
CurrentDisplayState.init(id); CurrentDisplayState.init(id);
KeyboardEnabledState.init(id); KeyboardEnabledState.init(id);
ShowRemoteCursorState.init(id); ShowRemoteCursorState.init(id);
RemoteCursorMovedState.init(id);
_showRemoteCursor = ShowRemoteCursorState.find(id); _showRemoteCursor = ShowRemoteCursorState.find(id);
_keyboardEnabled = KeyboardEnabledState.find(id); _keyboardEnabled = KeyboardEnabledState.find(id);
_remoteCursorMoved = RemoteCursorMovedState.find(id);
} }
void _removeStates(String id) { void _removeStates(String id) {
@ -70,6 +75,7 @@ class _RemotePageState extends State<RemotePage>
CurrentDisplayState.delete(id); CurrentDisplayState.delete(id);
ShowRemoteCursorState.delete(id); ShowRemoteCursorState.delete(id);
KeyboardEnabledState.delete(id); KeyboardEnabledState.delete(id);
RemoteCursorMovedState.delete(id);
} }
@override @override
@ -395,13 +401,14 @@ class _RemotePageState extends State<RemotePage>
id: widget.id, id: widget.id,
cursorOverImage: _cursorOverImage, cursorOverImage: _cursorOverImage,
keyboardEnabled: _keyboardEnabled, keyboardEnabled: _keyboardEnabled,
remoteCursorMoved: _remoteCursorMoved,
listenerBuilder: _buildImageListener, listenerBuilder: _buildImageListener,
); );
})) }))
]; ];
paints.add(Obx(() => Visibility( paints.add(Obx(() => Visibility(
visible: _keyboardEnabled.isTrue || _showRemoteCursor.isTrue, visible: _showRemoteCursor.isTrue && _remoteCursorMoved.isTrue,
child: CursorPaint( child: CursorPaint(
id: widget.id, id: widget.id,
)))); ))));
@ -459,6 +466,7 @@ class ImagePaint extends StatelessWidget {
final String id; final String id;
final Rx<bool> cursorOverImage; final Rx<bool> cursorOverImage;
final Rx<bool> keyboardEnabled; final Rx<bool> keyboardEnabled;
final Rx<bool> remoteCursorMoved;
final Widget Function(Widget)? listenerBuilder; final Widget Function(Widget)? listenerBuilder;
final ScrollController _horizontal = ScrollController(); final ScrollController _horizontal = ScrollController();
final ScrollController _vertical = ScrollController(); final ScrollController _vertical = ScrollController();
@ -468,6 +476,7 @@ class ImagePaint extends StatelessWidget {
required this.id, required this.id,
required this.cursorOverImage, required this.cursorOverImage,
required this.keyboardEnabled, required this.keyboardEnabled,
required this.remoteCursorMoved,
this.listenerBuilder}) this.listenerBuilder})
: super(key: key); : super(key: key);
@ -475,6 +484,7 @@ class ImagePaint extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final m = Provider.of<ImageModel>(context); final m = Provider.of<ImageModel>(context);
var c = Provider.of<CanvasModel>(context); var c = Provider.of<CanvasModel>(context);
final cursor = Provider.of<CursorModel>(context);
final s = c.scale; final s = c.scale;
if (c.scrollStyle == ScrollStyle.scrollbar) { if (c.scrollStyle == ScrollStyle.scrollbar) {
final imageWidget = SizedBox( final imageWidget = SizedBox(
@ -483,6 +493,8 @@ class ImagePaint extends StatelessWidget {
child: CustomPaint( child: CustomPaint(
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
)); ));
Rx<Offset> pos = Rx<Offset>(const Offset(0.0, 0.0));
return Center( return Center(
child: NotificationListener<ScrollNotification>( child: NotificationListener<ScrollNotification>(
onNotification: (notification) { onNotification: (notification) {
@ -498,9 +510,22 @@ class ImagePaint extends StatelessWidget {
return false; return false;
}, },
child: Obx(() => MouseRegion( child: Obx(() => MouseRegion(
cursor: (keyboardEnabled.isTrue && cursorOverImage.isTrue) cursor: (cursorOverImage.isTrue && keyboardEnabled.isTrue)
? (remoteCursorMoved.isTrue
? SystemMouseCursors.none ? SystemMouseCursors.none
: (cursor.pngData != null
? FlutterCustomMemoryImageCursor(
pixbuf: cursor.pngData!,
hotx: cursor.hotx,
hoty: cursor.hoty,
imageWidth: (cursor.image!.width * s).toInt(),
imageHeight: (cursor.image!.height * s).toInt(),
)
: MouseCursor.defer))
: MouseCursor.defer, : MouseCursor.defer,
onHover: (evt) {
pos.value = evt.position;
},
child: _buildCrossScrollbar(_buildListener(imageWidget)))), child: _buildCrossScrollbar(_buildListener(imageWidget)))),
), ),
); );

View File

@ -13,8 +13,10 @@ import '../../models/platform_model.dart';
import '../../models/server_model.dart'; import '../../models/server_model.dart';
class DesktopServerPage extends StatefulWidget { class DesktopServerPage extends StatefulWidget {
const DesktopServerPage({Key? key}) : super(key: key);
@override @override
State<StatefulWidget> createState() => _DesktopServerPageState(); State<DesktopServerPage> createState() => _DesktopServerPageState();
} }
class _DesktopServerPageState extends State<DesktopServerPage> class _DesktopServerPageState extends State<DesktopServerPage>

View File

@ -1031,6 +1031,7 @@ class PopupMenuButton<T> extends StatefulWidget {
Key? key, Key? key,
required this.itemBuilder, required this.itemBuilder,
this.initialValue, this.initialValue,
this.onHover,
this.onSelected, this.onSelected,
this.onCanceled, this.onCanceled,
this.tooltip, this.tooltip,
@ -1061,6 +1062,9 @@ class PopupMenuButton<T> extends StatefulWidget {
/// The value of the menu item, if any, that should be highlighted when the menu opens. /// The value of the menu item, if any, that should be highlighted when the menu opens.
final T? initialValue; final T? initialValue;
/// Called when the user hovers this button.
final ValueChanged<bool>? onHover;
/// Called when the user selects a value from the popup menu created by this button. /// Called when the user selects a value from the popup menu created by this button.
/// ///
/// If the popup menu is dismissed without selecting a value, [onCanceled] is /// If the popup menu is dismissed without selecting a value, [onCanceled] is
@ -1273,18 +1277,20 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
assert(debugCheckHasMaterialLocalizations(context)); assert(debugCheckHasMaterialLocalizations(context));
if (widget.child != null) if (widget.child != null) {
return Tooltip( return Tooltip(
message: message:
widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
child: InkWell( child: InkWell(
onTap: widget.enabled ? showButtonMenu : null, onTap: widget.enabled ? showButtonMenu : null,
onHover: widget.onHover,
canRequestFocus: _canRequestFocus, canRequestFocus: _canRequestFocus,
radius: widget.splashRadius, radius: widget.splashRadius,
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
child: widget.child, child: widget.child,
), ),
); );
}
return IconButton( return IconButton(
icon: widget.icon ?? Icon(Icons.adaptive.more), icon: widget.icon ?? Icon(Icons.adaptive.more),

View File

@ -427,7 +427,7 @@ abstract class BasePeerCard extends StatelessWidget {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: IconButton( child: IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
icon: Icon(Icons.edit), icon: const Icon(Icons.edit),
onPressed: () => _rdpDialog(id), onPressed: () => _rdpDialog(id),
), ),
)) ))
@ -440,6 +440,20 @@ abstract class BasePeerCard extends StatelessWidget {
); );
} }
@protected
MenuEntryBase<String> _wolAction(String id) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('WOL'),
style: style,
),
proc: () {
bind.mainWol(id: id);
},
dismissOnClicked: true,
);
}
@protected @protected
Future<MenuEntryBase<String>> _forceAlwaysRelayAction(String id) async { Future<MenuEntryBase<String>> _forceAlwaysRelayAction(String id) async {
const option = 'force-always-relay'; const option = 'force-always-relay';
@ -620,11 +634,16 @@ class RecentPeerCard extends BasePeerCard {
_transferFileAction(context, peer.id), _transferFileAction(context, peer.id),
_tcpTunnelingAction(context, peer.id), _tcpTunnelingAction(context, peer.id),
]; ];
MenuEntryBase<String>? rdpAction;
if (peer.platform == 'Windows') { if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id)); rdpAction = _rdpAction(context, peer.id);
} }
menuItems.add(MenuEntryDivider());
menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (rdpAction != null) {
menuItems.add(rdpAction);
}
menuItems.add(_wolAction(peer.id));
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id, false)); menuItems.add(_renameAction(peer.id, false));
menuItems.add(_removeAction(peer.id, () async { menuItems.add(_removeAction(peer.id, () async {
await bind.mainLoadRecentPeers(); await bind.mainLoadRecentPeers();
@ -647,10 +666,16 @@ class FavoritePeerCard extends BasePeerCard {
_transferFileAction(context, peer.id), _transferFileAction(context, peer.id),
_tcpTunnelingAction(context, peer.id), _tcpTunnelingAction(context, peer.id),
]; ];
MenuEntryBase<String>? rdpAction;
if (peer.platform == 'Windows') { if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id)); rdpAction = _rdpAction(context, peer.id);
} }
menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (rdpAction != null) {
menuItems.add(rdpAction);
}
menuItems.add(_wolAction(peer.id));
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id, false)); menuItems.add(_renameAction(peer.id, false));
menuItems.add(_removeAction(peer.id, () async { menuItems.add(_removeAction(peer.id, () async {
await bind.mainLoadFavPeers(); await bind.mainLoadFavPeers();
@ -673,10 +698,16 @@ class DiscoveredPeerCard extends BasePeerCard {
_transferFileAction(context, peer.id), _transferFileAction(context, peer.id),
_tcpTunnelingAction(context, peer.id), _tcpTunnelingAction(context, peer.id),
]; ];
MenuEntryBase<String>? rdpAction;
if (peer.platform == 'Windows') { if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id)); rdpAction = _rdpAction(context, peer.id);
} }
menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (rdpAction != null) {
menuItems.add(rdpAction);
}
menuItems.add(_wolAction(peer.id));
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id, false)); menuItems.add(_renameAction(peer.id, false));
menuItems.add(_removeAction(peer.id, () async { menuItems.add(_removeAction(peer.id, () async {
await bind.mainLoadLanPeers(); await bind.mainLoadLanPeers();
@ -698,10 +729,16 @@ class AddressBookPeerCard extends BasePeerCard {
_transferFileAction(context, peer.id), _transferFileAction(context, peer.id),
_tcpTunnelingAction(context, peer.id), _tcpTunnelingAction(context, peer.id),
]; ];
MenuEntryBase<String>? rdpAction;
if (peer.platform == 'Windows') { if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id)); rdpAction = _rdpAction(context, peer.id);
} }
menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (rdpAction != null) {
menuItems.add(rdpAction);
}
menuItems.add(_wolAction(peer.id));
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id, false)); menuItems.add(_renameAction(peer.id, false));
menuItems.add(_removeAction(peer.id, () async {})); menuItems.add(_removeAction(peer.id, () async {}));
menuItems.add(_unrememberPasswordAction(peer.id)); menuItems.add(_unrememberPasswordAction(peer.id));

View File

@ -12,7 +12,7 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
key, key,
this.height = kMinInteractiveDimension, this.height = kMinInteractiveDimension,
this.padding, this.padding,
this.enable = true, this.enabled,
this.textStyle, this.textStyle,
this.onTap, this.onTap,
this.position = mod_menu.PopupMenuPosition.overSide, this.position = mod_menu.PopupMenuPosition.overSide,
@ -25,7 +25,7 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
final Offset offset; final Offset offset;
final TextStyle? textStyle; final TextStyle? textStyle;
final EdgeInsets? padding; final EdgeInsets? padding;
final bool enable; final RxBool? enabled;
final void Function()? onTap; final void Function()? onTap;
final List<mod_menu.PopupMenuEntry<T>> Function(BuildContext) itemBuilder; final List<mod_menu.PopupMenuEntry<T>> Function(BuildContext) itemBuilder;
final Widget child; final Widget child;
@ -56,9 +56,9 @@ class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
TextStyle style = widget.textStyle ?? TextStyle style = widget.textStyle ??
popupMenuTheme.textStyle ?? popupMenuTheme.textStyle ??
theme.textTheme.subtitle1!; theme.textTheme.subtitle1!;
return Obx(() {
return mod_menu.PopupMenuButton<T>( return mod_menu.PopupMenuButton<T>(
enabled: widget.enable, enabled: widget.enabled != null ? widget.enabled!.value : true,
position: widget.position, position: widget.position,
offset: widget.offset, offset: widget.offset,
onSelected: handleTap, onSelected: handleTap,
@ -70,11 +70,13 @@ class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
child: Container( child: Container(
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: widget.height), constraints: BoxConstraints(minHeight: widget.height),
padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 16), padding:
widget.padding ?? const EdgeInsets.symmetric(horizontal: 16),
child: widget.child, child: widget.child,
), ),
), ),
); );
});
} }
} }
@ -98,8 +100,12 @@ class MenuConfig {
abstract class MenuEntryBase<T> { abstract class MenuEntryBase<T> {
bool dismissOnClicked; bool dismissOnClicked;
RxBool? enabled;
MenuEntryBase({this.dismissOnClicked = false}); MenuEntryBase({
this.dismissOnClicked = false,
this.enabled,
});
List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf); List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf);
} }
@ -119,9 +125,14 @@ class MenuEntryRadioOption {
String text; String text;
String value; String value;
bool dismissOnClicked; bool dismissOnClicked;
RxBool? enabled;
MenuEntryRadioOption( MenuEntryRadioOption({
{required this.text, required this.value, this.dismissOnClicked = false}); required this.text,
required this.value,
this.dismissOnClicked = false,
this.enabled,
});
} }
typedef RadioOptionsGetter = List<MenuEntryRadioOption> Function(); typedef RadioOptionsGetter = List<MenuEntryRadioOption> Function();
@ -138,13 +149,14 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
final RadioOptionSetter optionSetter; final RadioOptionSetter optionSetter;
final RxString _curOption = "".obs; final RxString _curOption = "".obs;
MenuEntryRadios( MenuEntryRadios({
{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}) dismissOnClicked = false,
: super(dismissOnClicked: dismissOnClicked) { RxBool? enabled,
}) : super(dismissOnClicked: dismissOnClicked, enabled: enabled) {
() async { () async {
_curOption.value = await curOptionGetter(); _curOption.value = await curOptionGetter();
}(); }();
@ -220,13 +232,17 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
final RadioOptionSetter optionSetter; final RadioOptionSetter optionSetter;
final RxString _curOption = "".obs; final RxString _curOption = "".obs;
MenuEntrySubRadios( MenuEntrySubRadios({
{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}) dismissOnClicked = false,
: super(dismissOnClicked: dismissOnClicked) { RxBool? enabled,
}) : super(
dismissOnClicked: dismissOnClicked,
enabled: enabled,
) {
() async { () async {
_curOption.value = await curOptionGetter(); _curOption.value = await curOptionGetter();
}(); }();
@ -293,6 +309,7 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
BuildContext context, MenuConfig conf) { BuildContext context, MenuConfig conf) {
return [ return [
PopupMenuChildrenItem( PopupMenuChildrenItem(
enabled: super.enabled,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
height: conf.height, height: conf.height,
itemBuilder: (BuildContext context) => itemBuilder: (BuildContext context) =>
@ -325,9 +342,14 @@ 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;
final Rx<TextStyle>? textStyle;
MenuEntrySwitchBase({required this.text, required dismissOnClicked}) MenuEntrySwitchBase({
: super(dismissOnClicked: dismissOnClicked); required this.text,
required dismissOnClicked,
this.textStyle,
RxBool? enabled,
}) : super(dismissOnClicked: dismissOnClicked, enabled: enabled);
RxBool get curOption; RxBool get curOption;
Future<void> setOption(bool option); Future<void> setOption(bool option);
@ -344,14 +366,23 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
height: conf.height, height: conf.height,
child: Row(children: [ child: Row(children: [
// const SizedBox(width: MenuConfig.midPadding), () {
Text( if (textStyle != null) {
final style = textStyle!;
return Obx(() => Text(
text, text,
style: TextStyle( style: style.value,
color: MyTheme.color(context).text, ));
} else {
return Text(
text,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize, fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal), fontWeight: FontWeight.normal),
), );
}
}(),
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
@ -384,12 +415,19 @@ class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
final SwitchSetter setter; final SwitchSetter setter;
final RxBool _curOption = false.obs; final RxBool _curOption = false.obs;
MenuEntrySwitch( MenuEntrySwitch({
{required String text, required String text,
required this.getter, required this.getter,
required this.setter, required this.setter,
dismissOnClicked = false}) Rx<TextStyle>? textStyle,
: super(text: text, dismissOnClicked: dismissOnClicked) { dismissOnClicked = false,
RxBool? enabled,
}) : super(
text: text,
textStyle: textStyle,
dismissOnClicked: dismissOnClicked,
enabled: enabled,
) {
() async { () async {
_curOption.value = await getter(); _curOption.value = await getter();
}(); }();
@ -414,12 +452,17 @@ class MenuEntrySwitch2<T> extends MenuEntrySwitchBase<T> {
final Switch2Getter getter; final Switch2Getter getter;
final SwitchSetter setter; final SwitchSetter setter;
MenuEntrySwitch2( MenuEntrySwitch2({
{required String text, required String text,
required this.getter, required this.getter,
required this.setter, required this.setter,
dismissOnClicked = false}) Rx<TextStyle>? textStyle,
: super(text: text, dismissOnClicked: dismissOnClicked); dismissOnClicked = false,
RxBool? enabled,
}) : super(
text: text,
textStyle: textStyle,
dismissOnClicked: dismissOnClicked);
@override @override
RxBool get curOption => getter(); RxBool get curOption => getter();
@ -433,13 +476,18 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
final String text; final String text;
final List<MenuEntryBase<T>> entries; final List<MenuEntryBase<T>> entries;
MenuEntrySubMenu({required this.text, required this.entries}); MenuEntrySubMenu({
required this.text,
required this.entries,
RxBool? enabled,
}) : super(enabled: enabled);
@override @override
List<mod_menu.PopupMenuEntry<T>> build( List<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) { BuildContext context, MenuConfig conf) {
return [ return [
PopupMenuChildrenItem( PopupMenuChildrenItem(
enabled: super.enabled,
height: conf.height, height: conf.height,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
position: mod_menu.PopupMenuPosition.overSide, position: mod_menu.PopupMenuPosition.overSide,
@ -449,20 +497,24 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
.toList(), .toList(),
child: Row(children: [ child: Row(children: [
const SizedBox(width: MenuConfig.midPadding), const SizedBox(width: MenuConfig.midPadding),
Text( Obx(() => Text(
text, text,
style: TextStyle( style: TextStyle(
color: MyTheme.color(context).text, color: (super.enabled != null ? super.enabled!.value : true)
? Colors.black
: Colors.grey,
fontSize: MenuConfig.fontSize, fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal), fontWeight: FontWeight.normal),
), )),
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Icon( child: Obx(() => Icon(
Icons.keyboard_arrow_right, Icons.keyboard_arrow_right,
color: conf.commonColor, color: (super.enabled != null ? super.enabled!.value : true)
), ? conf.commonColor
: Colors.grey,
)),
)) ))
]), ]),
) )
@ -474,36 +526,57 @@ 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}) dismissOnClicked = false,
: super(dismissOnClicked: dismissOnClicked); RxBool? enabled,
}) : super(
dismissOnClicked: dismissOnClicked,
enabled: enabled,
);
Widget _buildChild(BuildContext context, MenuConfig conf) {
return Obx(() {
bool enabled = true;
if (super.enabled != null) {
enabled = super.enabled!.value;
}
const enabledStyle = TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal);
const disabledStyle = TextStyle(
color: Colors.grey,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal);
return TextButton(
onPressed: enabled
? () {
if (super.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
}
proc();
}
: null,
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.height),
child: childBuilder(enabled ? enabledStyle : disabledStyle),
),
);
});
}
@override @override
List<mod_menu.PopupMenuEntry<T>> build( List<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) { BuildContext context, MenuConfig conf) {
return [ return [
mod_menu.PopupMenuItem( mod_menu.PopupMenuItem(
enabled: super.enabled != null ? super.enabled!.value : true,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
height: conf.height, height: conf.height,
child: TextButton( child: _buildChild(context, conf),
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.height),
child: childBuilder(
TextStyle(
color: MyTheme.color(context).text,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
)),
onPressed: () {
if (super.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
}
proc();
},
),
) )
]; ];
} }

View File

@ -75,7 +75,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
final List<Widget> menubarItems = []; final List<Widget> menubarItems = [];
if (!isWebDesktop) { if (!isWebDesktop) {
menubarItems.add(_buildFullscreen(context)); menubarItems.add(_buildFullscreen(context));
if (widget.ffi.ffiModel.isPeerAndroid) { //if (widget.ffi.ffiModel.isPeerAndroid) {
menubarItems.add(IconButton( menubarItems.add(IconButton(
tooltip: translate('Mobile Actions'), tooltip: translate('Mobile Actions'),
color: _MenubarTheme.commonColor, color: _MenubarTheme.commonColor,
@ -88,7 +88,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
} }
}, },
)); ));
} //}
} }
menubarItems.add(_buildMonitor(context)); menubarItems.add(_buildMonitor(context));
menubarItems.add(_buildControl(context)); menubarItems.add(_buildControl(context));

View File

@ -4,6 +4,7 @@ import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -54,7 +55,7 @@ class FfiModel with ChangeNotifier {
bool get touchMode => _touchMode; bool get touchMode => _touchMode;
bool get isPeerAndroid => _pi.platform == "Android"; bool get isPeerAndroid => _pi.platform == 'Android';
set inputBlocked(v) { set inputBlocked(v) {
_inputBlocked = v; _inputBlocked = v;
@ -116,7 +117,7 @@ class FfiModel with ChangeNotifier {
return null; return null;
} else { } else {
final icon = final icon =
'${secure == true ? "secure" : "insecure"}${direct == true ? "" : "_relay"}'; '${secure == true ? 'secure' : 'insecure'}${direct == true ? '' : '_relay'}';
return Image.asset('assets/$icon.png', width: 48, height: 48); return Image.asset('assets/$icon.png', width: 48, height: 48);
} }
} }
@ -143,17 +144,17 @@ class FfiModel with ChangeNotifier {
} else if (name == 'cursor_id') { } else if (name == 'cursor_id') {
parent.target?.cursorModel.updateCursorId(evt); parent.target?.cursorModel.updateCursorId(evt);
} else if (name == 'cursor_position') { } else if (name == 'cursor_position') {
parent.target?.cursorModel.updateCursorPosition(evt); parent.target?.cursorModel.updateCursorPosition(evt, peerId);
} else if (name == 'clipboard') { } else if (name == 'clipboard') {
Clipboard.setData(ClipboardData(text: evt['content'])); Clipboard.setData(ClipboardData(text: evt['content']));
} else if (name == 'permission') { } else if (name == 'permission') {
parent.target?.ffiModel.updatePermission(evt, peerId); parent.target?.ffiModel.updatePermission(evt, peerId);
} else if (name == 'chat_client_mode') { } else if (name == 'chat_client_mode') {
parent.target?.chatModel parent.target?.chatModel
.receive(ChatModel.clientModeID, evt['text'] ?? ""); .receive(ChatModel.clientModeID, evt['text'] ?? '');
} else if (name == 'chat_server_mode') { } else if (name == 'chat_server_mode') {
parent.target?.chatModel parent.target?.chatModel
.receive(int.parse(evt['id'] as String), evt['text'] ?? ""); .receive(int.parse(evt['id'] as String), evt['text'] ?? '');
} else if (name == 'file_dir') { } else if (name == 'file_dir') {
parent.target?.fileModel.receiveFileDir(evt); parent.target?.fileModel.receiveFileDir(evt);
} else if (name == 'job_progress') { } else if (name == 'job_progress') {
@ -184,61 +185,7 @@ class FfiModel with ChangeNotifier {
/// Bind the event listener to receive events from the Rust core. /// Bind the event listener to receive events from the Rust core.
void updateEventListener(String peerId) { void updateEventListener(String peerId) {
cb(evt) { platformFFI.setEventCallback(startEventListener(peerId));
var name = evt['name'];
if (name == 'msgbox') {
handleMsgBox(evt, peerId);
} else if (name == 'peer_info') {
handlePeerInfo(evt, peerId);
} else if (name == 'connection_ready') {
parent.target?.ffiModel.setConnectionType(
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
} else if (name == 'switch_display') {
handleSwitchDisplay(evt);
} else if (name == 'cursor_data') {
parent.target?.cursorModel.updateCursorData(evt);
} else if (name == 'cursor_id') {
parent.target?.cursorModel.updateCursorId(evt);
} else if (name == 'cursor_position') {
parent.target?.cursorModel.updateCursorPosition(evt);
} else if (name == 'clipboard') {
Clipboard.setData(ClipboardData(text: evt['content']));
} else if (name == 'permission') {
parent.target?.ffiModel.updatePermission(evt, peerId);
} else if (name == 'chat_client_mode') {
parent.target?.chatModel
.receive(ChatModel.clientModeID, evt['text'] ?? "");
} else if (name == 'chat_server_mode') {
parent.target?.chatModel
.receive(int.parse(evt['id'] as String), evt['text'] ?? "");
} else if (name == 'file_dir') {
parent.target?.fileModel.receiveFileDir(evt);
} else if (name == 'job_progress') {
parent.target?.fileModel.tryUpdateJobProgress(evt);
} else if (name == 'job_done') {
parent.target?.fileModel.jobDone(evt);
} else if (name == 'job_error') {
parent.target?.fileModel.jobError(evt);
} else if (name == 'override_file_confirm') {
parent.target?.fileModel.overrideFileConfirm(evt);
} else if (name == 'load_last_job') {
parent.target?.fileModel.loadLastJob(evt);
} else if (name == 'update_folder_files') {
parent.target?.fileModel.updateFolderFiles(evt);
} else if (name == 'add_connection') {
parent.target?.serverModel.addConnection(evt);
} else if (name == 'on_client_remove') {
parent.target?.serverModel.onClientRemove(evt);
} else if (name == 'update_quality_status') {
parent.target?.qualityMonitorModel.updateQualityStatus(evt);
} else if (name == 'update_block_input_state') {
updateBlockInputState(evt, peerId);
} else if (name == 'update_privacy_mode') {
updatePrivacyMode(evt, peerId);
}
}
platformFFI.setEventCallback(cb);
} }
void handleSwitchDisplay(Map<String, dynamic> evt) { void handleSwitchDisplay(Map<String, dynamic> evt) {
@ -249,8 +196,9 @@ class FfiModel with ChangeNotifier {
_display.y = double.parse(evt['y']); _display.y = double.parse(evt['y']);
_display.width = int.parse(evt['width']); _display.width = int.parse(evt['width']);
_display.height = int.parse(evt['height']); _display.height = int.parse(evt['height']);
if (old != _pi.currentDisplay) if (old != _pi.currentDisplay) {
parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y);
}
// remote is mobile, and orientation changed // remote is mobile, and orientation changed
if ((_display.width > _display.height) != oldOrientation) { if ((_display.width > _display.height) != oldOrientation) {
@ -307,7 +255,7 @@ class FfiModel with ChangeNotifier {
_pi.username = evt['username']; _pi.username = evt['username'];
_pi.hostname = evt['hostname']; _pi.hostname = evt['hostname'];
_pi.platform = evt['platform']; _pi.platform = evt['platform'];
_pi.sasEnabled = evt['sas_enabled'] == "true"; _pi.sasEnabled = evt['sas_enabled'] == 'true';
_pi.currentDisplay = int.parse(evt['current_display']); _pi.currentDisplay = int.parse(evt['current_display']);
try { try {
@ -323,7 +271,7 @@ class FfiModel with ChangeNotifier {
} }
} else { } else {
_touchMode = _touchMode =
await bind.sessionGetOption(id: peerId, arg: "touch-mode") != ''; await bind.sessionGetOption(id: peerId, arg: 'touch-mode') != '';
} }
if (parent.target != null && if (parent.target != null &&
@ -381,7 +329,7 @@ class ImageModel with ChangeNotifier {
ui.Image? get image => _image; ui.Image? get image => _image;
String _id = ""; String _id = '';
WeakReference<FFI> parent; WeakReference<FFI> parent;
@ -426,7 +374,7 @@ class ImageModel with ChangeNotifier {
} }
Future.delayed(Duration(milliseconds: 1), () { Future.delayed(Duration(milliseconds: 1), () {
if (parent.target?.ffiModel.isPeerAndroid ?? false) { if (parent.target?.ffiModel.isPeerAndroid ?? false) {
bind.sessionPeerOption(id: _id, name: "view-style", value: "shrink"); bind.sessionPeerOption(id: _id, name: 'view-style', value: 'shrink');
parent.target?.canvasModel.updateViewStyle(); parent.target?.canvasModel.updateViewStyle();
} }
}); });
@ -471,7 +419,7 @@ class CanvasModel with ChangeNotifier {
// the tabbar over the image // the tabbar over the image
double tabBarHeight = 0.0; double tabBarHeight = 0.0;
// TODO multi canvas model // TODO multi canvas model
String id = ""; String id = '';
// scroll offset x percent // scroll offset x percent
double _scrollX = 0.0; double _scrollX = 0.0;
// scroll offset y percent // scroll offset y percent
@ -580,9 +528,16 @@ class CanvasModel with ChangeNotifier {
} }
// If keyboard is not permitted, do not move cursor when mouse is moving. // If keyboard is not permitted, do not move cursor when mouse is moving.
if (parent.target != null) { if (parent.target != null && parent.target!.ffiModel.keyboard()) {
if (parent.target!.ffiModel.keyboard()) { // Draw cursor if is not desktop.
if (!isDesktop) {
parent.target!.cursorModel.moveLocal(x, y); parent.target!.cursorModel.moveLocal(x, y);
} else {
try {
RemoteCursorMovedState.find(id).value = false;
} catch (e) {
//
}
} }
} }
} }
@ -642,16 +597,19 @@ class CanvasModel with ChangeNotifier {
class CursorModel with ChangeNotifier { class CursorModel with ChangeNotifier {
ui.Image? _image; ui.Image? _image;
final _images = <int, Tuple3<ui.Image, double, double>>{}; final _images = <int, Tuple3<ui.Image, double, double>>{};
Uint8List? _pngData;
final _pngs = <int, Uint8List?>{};
double _x = -10000; double _x = -10000;
double _y = -10000; double _y = -10000;
double _hotx = 0; double _hotx = 0;
double _hoty = 0; double _hoty = 0;
double _displayOriginX = 0; double _displayOriginX = 0;
double _displayOriginY = 0; double _displayOriginY = 0;
String id = ""; // TODO multi cursor model String id = ''; // TODO multi cursor model
WeakReference<FFI> parent; WeakReference<FFI> parent;
ui.Image? get image => _image; ui.Image? get image => _image;
Uint8List? get pngData => _pngData;
double get x => _x - _displayOriginX; double get x => _x - _displayOriginX;
@ -801,19 +759,29 @@ class CursorModel with ChangeNotifier {
var pid = parent.target?.id; var pid = parent.target?.id;
ui.decodeImageFromPixels(rgba, width, height, ui.PixelFormat.rgba8888, ui.decodeImageFromPixels(rgba, width, height, ui.PixelFormat.rgba8888,
(image) { (image) {
() async {
if (parent.target?.id != pid) return; if (parent.target?.id != pid) return;
_image = image; _image = image;
_images[id] = Tuple3(image, _hotx, _hoty); _images[id] = Tuple3(image, _hotx, _hoty);
final data = await image.toByteData(format: ImageByteFormat.png);
if (data != null) {
_pngData = data.buffer.asUint8List();
} else {
_pngData = null;
}
_pngs[id] = _pngData;
try { try {
// my throw exception, because the listener maybe already dispose // my throw exception, because the listener maybe already dispose
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
debugPrint('notify cursor: $e'); debugPrint('notify cursor: $e');
} }
}();
}); });
} }
void updateCursorId(Map<String, dynamic> evt) { void updateCursorId(Map<String, dynamic> evt) {
_pngData = _pngs[int.parse(evt['id'])];
final tmp = _images[int.parse(evt['id'])]; final tmp = _images[int.parse(evt['id'])];
if (tmp != null) { if (tmp != null) {
_image = tmp.item1; _image = tmp.item1;
@ -824,9 +792,14 @@ class CursorModel with ChangeNotifier {
} }
/// Update the cursor position. /// Update the cursor position.
void updateCursorPosition(Map<String, dynamic> evt) { void updateCursorPosition(Map<String, dynamic> evt, String id) {
_x = double.parse(evt['x']); _x = double.parse(evt['x']);
_y = double.parse(evt['y']); _y = double.parse(evt['y']);
try {
RemoteCursorMovedState.find(id).value = true;
} catch (e) {
//
}
notifyListeners(); notifyListeners();
} }
@ -888,13 +861,15 @@ class QualityMonitorModel with ChangeNotifier {
updateQualityStatus(Map<String, dynamic> evt) { updateQualityStatus(Map<String, dynamic> evt) {
try { try {
if ((evt["speed"] as String).isNotEmpty) _data.speed = evt["speed"]; if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed'];
if ((evt["fps"] as String).isNotEmpty) _data.fps = evt["fps"]; if ((evt['fps'] as String).isNotEmpty) _data.fps = evt['fps'];
if ((evt["delay"] as String).isNotEmpty) _data.delay = evt["delay"]; if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay'];
if ((evt["target_bitrate"] as String).isNotEmpty) if ((evt['target_bitrate'] as String).isNotEmpty) {
_data.targetBitrate = evt["target_bitrate"]; _data.targetBitrate = evt['target_bitrate'];
if ((evt["codec_format"] as String).isNotEmpty) }
_data.codecFormat = evt["codec_format"]; if ((evt['codec_format'] as String).isNotEmpty) {
_data.codecFormat = evt['codec_format'];
}
notifyListeners(); notifyListeners();
} catch (e) {} } catch (e) {}
} }
@ -907,11 +882,11 @@ extension ToString on MouseButtons {
String get value { String get value {
switch (this) { switch (this) {
case MouseButtons.left: case MouseButtons.left:
return "left"; return 'left';
case MouseButtons.right: case MouseButtons.right:
return "right"; return 'right';
case MouseButtons.wheel: case MouseButtons.wheel:
return "wheel"; return 'wheel';
} }
} }
} }
@ -920,12 +895,12 @@ enum ConnType { defaultConn, fileTransfer, portForward, rdp }
/// FFI class for communicating with the Rust core. /// FFI class for communicating with the Rust core.
class FFI { class FFI {
var id = ""; var id = '';
var shift = false; var shift = false;
var ctrl = false; var ctrl = false;
var alt = false; var alt = false;
var command = false; var command = false;
var version = ""; var version = '';
var connType = ConnType.defaultConn; var connType = ConnType.defaultConn;
/// dialogManager use late to ensure init after main page binding [globalKey] /// dialogManager use late to ensure init after main page binding [globalKey]
@ -1006,11 +981,11 @@ class FFI {
// out['name'] = name; // out['name'] = name;
// // default: down = false // // default: down = false
// if (down == true) { // if (down == true) {
// out['down'] = "true"; // out['down'] = 'true';
// } // }
// // default: press = true // // default: press = true
// if (press != false) { // if (press != false) {
// out['press'] = "true"; // out['press'] = 'true';
// } // }
// setByName('input_key', json.encode(modify(out))); // setByName('input_key', json.encode(modify(out)));
// TODO id // TODO id
@ -1038,7 +1013,7 @@ class FFI {
Future<List<Peer>> peers() async { Future<List<Peer>> peers() async {
try { try {
var str = await bind.mainGetRecentPeers(); var str = await bind.mainGetRecentPeers();
if (str == "") return []; if (str == '') return [];
List<dynamic> peers = json.decode(str); List<dynamic> peers = json.decode(str);
return peers return peers
.map((s) => s as List<dynamic>) .map((s) => s as List<dynamic>)
@ -1046,7 +1021,7 @@ class FFI {
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 [];
} }
@ -1056,7 +1031,7 @@ class FFI {
{bool isFileTransfer = false, {bool isFileTransfer = false,
bool isPortForward = false, bool isPortForward = false,
double tabBarHeight = 0.0}) { double tabBarHeight = 0.0}) {
assert(!(isFileTransfer && isPortForward), "more than one connect type"); assert(!(isFileTransfer && isPortForward), 'more than one connect type');
if (isFileTransfer) { if (isFileTransfer) {
connType = ConnType.fileTransfer; connType = ConnType.fileTransfer;
id = 'ft_$id'; id = 'ft_$id';
@ -1108,13 +1083,13 @@ class FFI {
canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay);
} }
bind.sessionClose(id: id); bind.sessionClose(id: id);
id = ""; id = '';
imageModel.update(null, 0.0); imageModel.update(null, 0.0);
cursorModel.clear(); cursorModel.clear();
ffiModel.clear(); ffiModel.clear();
canvasModel.clear(); canvasModel.clear();
resetModifiers(); resetModifiers();
debugPrint("model $id closed"); debugPrint('model $id closed');
} }
/// Send **get** command to the Rust core based on [name] and [arg]. /// Send **get** command to the Rust core based on [name] and [arg].
@ -1221,7 +1196,7 @@ class FFI {
Future<String> getDefaultAudioInput() async { Future<String> getDefaultAudioInput() async {
final input = await bind.mainGetOption(key: 'audio-input'); final input = await bind.mainGetOption(key: 'audio-input');
if (input.isEmpty && Platform.isWindows) { if (input.isEmpty && Platform.isWindows) {
return "System Sound"; return 'System Sound';
} }
return input; return input;
} }
@ -1232,8 +1207,8 @@ class FFI {
Future<Map<String, String>> getHttpHeaders() async { Future<Map<String, String>> getHttpHeaders() async {
return { return {
"Authorization": 'Authorization':
"Bearer " + await bind.mainGetLocalOption(key: "access_token") 'Bearer ' + await bind.mainGetLocalOption(key: 'access_token')
}; };
} }
} }
@ -1246,10 +1221,10 @@ class Display {
} }
class PeerInfo { class PeerInfo {
String version = ""; String version = '';
String username = ""; String username = '';
String hostname = ""; String hostname = '';
String platform = ""; String platform = '';
bool sasEnabled = false; bool sasEnabled = false;
int currentDisplay = 0; int currentDisplay = 0;
List<Display> displays = []; List<Display> displays = [];

View File

@ -35,7 +35,7 @@ class ServerModel with ChangeNotifier {
final tabController = DesktopTabController(tabType: DesktopTabType.cm); final tabController = DesktopTabController(tabType: DesktopTabType.cm);
List<Client> _clients = []; final List<Client> _clients = [];
bool get isStart => _isStart; bool get isStart => _isStart;
@ -61,8 +61,8 @@ class ServerModel with ChangeNotifier {
return _verificationMethod; return _verificationMethod;
} }
set verificationMethod(String method) { setVerificationMethod(String method) async {
bind.mainSetOption(key: "verification-method", value: method); await bind.mainSetOption(key: "verification-method", value: method);
} }
String get temporaryPasswordLength { String get temporaryPasswordLength {
@ -73,8 +73,8 @@ class ServerModel with ChangeNotifier {
return _temporaryPasswordLength; return _temporaryPasswordLength;
} }
set temporaryPasswordLength(String length) { setTemporaryPasswordLength(String length) async {
bind.mainSetOption(key: "temporary-password-length", value: length); await bind.mainSetOption(key: "temporary-password-length", value: length);
} }
TextEditingController get serverId => _serverId; TextEditingController get serverId => _serverId;

View File

@ -68,6 +68,10 @@ dependencies:
git: git:
url: https://github.com/Kingtous/rustdesk_tray_manager url: https://github.com/Kingtous/rustdesk_tray_manager
ref: 3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a ref: 3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a
flutter_custom_cursor:
git:
url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor
ref: 7fe78c139c711bafbae52d924e9caf18bd193e28
get: ^4.6.5 get: ^4.6.5
visibility_detector: ^0.3.3 visibility_detector: ^0.3.3
contextmenu: ^3.0.0 contextmenu: ^3.0.0

View File

@ -789,6 +789,10 @@ pub fn main_get_mouse_time() -> f64 {
get_mouse_time() get_mouse_time()
} }
pub fn main_wol(id: String) {
crate::lan::send_wol(id)
}
pub fn cm_send_chat(conn_id: i32, msg: String) { pub fn cm_send_chat(conn_id: i32, msg: String) {
crate::ui_cm_interface::send_chat(conn_id, msg); crate::ui_cm_interface::send_chat(conn_id, msg);
} }