mirror of
https://github.com/rustdesk/rustdesk.git
synced 2024-11-27 23:19:02 +08:00
Merge pull request #1468 from fufesou/flutter_desktop_new_remote_menu_3
Flutter desktop cursor & popup menu refactor
This commit is contained in:
commit
7d60992770
@ -1,7 +1,8 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../consts.dart';
|
||||
import '../models/platform_model.dart';
|
||||
|
||||
// TODO: A lot of dup code.
|
||||
|
||||
class PrivacyModeState {
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
@ -22,10 +22,10 @@ import '../../models/platform_model.dart';
|
||||
|
||||
/// Connection page for connecting to a remote peer.
|
||||
class ConnectionPage extends StatefulWidget {
|
||||
ConnectionPage({Key? key}) : super(key: key);
|
||||
const ConnectionPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ConnectionPageState createState() => _ConnectionPageState();
|
||||
State<ConnectionPage> createState() => _ConnectionPageState();
|
||||
}
|
||||
|
||||
/// State for the connection page.
|
||||
@ -101,7 +101,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
],
|
||||
).marginSymmetric(horizontal: 22),
|
||||
),
|
||||
Divider(),
|
||||
const Divider(),
|
||||
SizedBox(height: 50, child: Obx(() => buildStatus()))
|
||||
.paddingSymmetric(horizontal: 12.0)
|
||||
]),
|
||||
|
@ -19,11 +19,18 @@ import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:url_launcher/url_launcher_string.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 {
|
||||
DesktopHomePage({Key? key}) : super(key: key);
|
||||
const DesktopHomePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DesktopHomePageState();
|
||||
State<DesktopHomePage> createState() => _DesktopHomePageState();
|
||||
}
|
||||
|
||||
const borderColor = Color(0xFF2F65BA);
|
||||
@ -109,7 +116,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
Container(
|
||||
height: 25,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@ -135,11 +142,11 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
child: TextFormField(
|
||||
controller: model.serverId,
|
||||
readOnly: true,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.only(bottom: 20),
|
||||
),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
),
|
||||
),
|
||||
@ -322,18 +329,19 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
onTap: () => bind.mainUpdateTemporaryPassword(),
|
||||
onHover: (value) => refreshHover.value = value,
|
||||
),
|
||||
FutureBuilder<Widget>(
|
||||
future: buildPasswordPopupMenu(context),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
print("${snapshot.error}");
|
||||
}
|
||||
if (snapshot.hasData) {
|
||||
return snapshot.data!;
|
||||
} else {
|
||||
return Offstage();
|
||||
}
|
||||
})
|
||||
const _PasswordPopupMenu(),
|
||||
// FutureBuilder<Widget>(
|
||||
// future: buildPasswordPopupMenu(context),
|
||||
// builder: (context, snapshot) {
|
||||
// if (snapshot.hasError) {
|
||||
// print("${snapshot.error}");
|
||||
// }
|
||||
// if (snapshot.hasData) {
|
||||
// return snapshot.data!;
|
||||
// } 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 =
|
||||
gFFI.serverModel.verificationMethod != kUsePermanentPassword;
|
||||
@ -403,8 +411,11 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
onTap: () {
|
||||
if (gFFI.serverModel.temporaryPasswordLength !=
|
||||
e) {
|
||||
gFFI.serverModel.temporaryPasswordLength = e;
|
||||
bind.mainUpdateTemporaryPassword();
|
||||
() async {
|
||||
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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -314,8 +313,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
translate("Use permanent password"),
|
||||
translate("Use both passwords"),
|
||||
];
|
||||
bool tmp_enabled = model.verificationMethod != kUsePermanentPassword;
|
||||
bool perm_enabled = model.verificationMethod != kUseTemporaryPassword;
|
||||
bool tmpEnabled = model.verificationMethod != kUsePermanentPassword;
|
||||
bool permEnabled = model.verificationMethod != kUseTemporaryPassword;
|
||||
String currentValue = values[keys.indexOf(model.verificationMethod)];
|
||||
List<Widget> radios = values
|
||||
.map((value) => _Radio<String>(
|
||||
@ -324,16 +323,24 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
groupValue: currentValue,
|
||||
label: value,
|
||||
onChanged: ((value) {
|
||||
model.verificationMethod = keys[values.indexOf(value)];
|
||||
() async {
|
||||
await model
|
||||
.setVerificationMethod(keys[values.indexOf(value)]);
|
||||
await model.updatePasswordModel();
|
||||
}();
|
||||
}),
|
||||
enabled: !locked,
|
||||
))
|
||||
.toList();
|
||||
|
||||
var onChanged = tmp_enabled && !locked
|
||||
var onChanged = tmpEnabled && !locked
|
||||
? (value) {
|
||||
if (value != null)
|
||||
model.temporaryPasswordLength = value.toString();
|
||||
if (value != null) {
|
||||
() async {
|
||||
await model.setTemporaryPasswordLength(value.toString());
|
||||
await model.updatePasswordModel();
|
||||
}();
|
||||
}
|
||||
}
|
||||
: null;
|
||||
List<Widget> lengthRadios = ['6', '8', '10']
|
||||
@ -365,10 +372,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
...lengthRadios,
|
||||
],
|
||||
),
|
||||
enabled: tmp_enabled && !locked),
|
||||
enabled: tmpEnabled && !locked),
|
||||
radios[1],
|
||||
_SubButton('Set permanent password', setPasswordDialog,
|
||||
perm_enabled && !locked),
|
||||
permEnabled && !locked),
|
||||
radios[2],
|
||||
]);
|
||||
})));
|
||||
|
@ -17,7 +17,7 @@ import '../../models/platform_model.dart';
|
||||
enum LocationStatus { bread, textField }
|
||||
|
||||
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;
|
||||
|
||||
@override
|
||||
|
@ -21,8 +21,8 @@ class FileManagerTabPage extends StatefulWidget {
|
||||
class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
DesktopTabController get tabController => Get.find<DesktopTabController>();
|
||||
|
||||
static final IconData selectedIcon = Icons.file_copy_sharp;
|
||||
static final IconData unselectedIcon = Icons.file_copy_outlined;
|
||||
static const IconData selectedIcon = Icons.file_copy_sharp;
|
||||
static const IconData unselectedIcon = Icons.file_copy_outlined;
|
||||
|
||||
_FileManagerTabPageState(Map<String, dynamic> params) {
|
||||
Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer));
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
@ -8,6 +9,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
import 'package:flutter_custom_cursor/flutter_custom_cursor.dart';
|
||||
|
||||
// import 'package:window_manager/window_manager.dart';
|
||||
|
||||
@ -22,7 +24,7 @@ import '../../common/shared_state.dart';
|
||||
final initText = '\1' * 1024;
|
||||
|
||||
class RemotePage extends StatefulWidget {
|
||||
RemotePage({
|
||||
const RemotePage({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.tabBarHeight,
|
||||
@ -32,7 +34,7 @@ class RemotePage extends StatefulWidget {
|
||||
final double tabBarHeight;
|
||||
|
||||
@override
|
||||
_RemotePageState createState() => _RemotePageState();
|
||||
State<RemotePage> createState() => _RemotePageState();
|
||||
}
|
||||
|
||||
class _RemotePageState extends State<RemotePage>
|
||||
@ -41,6 +43,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
String _value = '';
|
||||
final _cursorOverImage = false.obs;
|
||||
late RxBool _showRemoteCursor;
|
||||
late RxBool _remoteCursorMoved;
|
||||
late RxBool _keyboardEnabled;
|
||||
|
||||
final FocusNode _mobileFocusNode = FocusNode();
|
||||
@ -60,8 +63,10 @@ class _RemotePageState extends State<RemotePage>
|
||||
CurrentDisplayState.init(id);
|
||||
KeyboardEnabledState.init(id);
|
||||
ShowRemoteCursorState.init(id);
|
||||
RemoteCursorMovedState.init(id);
|
||||
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
||||
_keyboardEnabled = KeyboardEnabledState.find(id);
|
||||
_remoteCursorMoved = RemoteCursorMovedState.find(id);
|
||||
}
|
||||
|
||||
void _removeStates(String id) {
|
||||
@ -70,6 +75,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
CurrentDisplayState.delete(id);
|
||||
ShowRemoteCursorState.delete(id);
|
||||
KeyboardEnabledState.delete(id);
|
||||
RemoteCursorMovedState.delete(id);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -395,13 +401,14 @@ class _RemotePageState extends State<RemotePage>
|
||||
id: widget.id,
|
||||
cursorOverImage: _cursorOverImage,
|
||||
keyboardEnabled: _keyboardEnabled,
|
||||
remoteCursorMoved: _remoteCursorMoved,
|
||||
listenerBuilder: _buildImageListener,
|
||||
);
|
||||
}))
|
||||
];
|
||||
|
||||
paints.add(Obx(() => Visibility(
|
||||
visible: _keyboardEnabled.isTrue || _showRemoteCursor.isTrue,
|
||||
visible: _showRemoteCursor.isTrue && _remoteCursorMoved.isTrue,
|
||||
child: CursorPaint(
|
||||
id: widget.id,
|
||||
))));
|
||||
@ -459,6 +466,7 @@ class ImagePaint extends StatelessWidget {
|
||||
final String id;
|
||||
final Rx<bool> cursorOverImage;
|
||||
final Rx<bool> keyboardEnabled;
|
||||
final Rx<bool> remoteCursorMoved;
|
||||
final Widget Function(Widget)? listenerBuilder;
|
||||
final ScrollController _horizontal = ScrollController();
|
||||
final ScrollController _vertical = ScrollController();
|
||||
@ -468,6 +476,7 @@ class ImagePaint extends StatelessWidget {
|
||||
required this.id,
|
||||
required this.cursorOverImage,
|
||||
required this.keyboardEnabled,
|
||||
required this.remoteCursorMoved,
|
||||
this.listenerBuilder})
|
||||
: super(key: key);
|
||||
|
||||
@ -475,6 +484,7 @@ class ImagePaint extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final m = Provider.of<ImageModel>(context);
|
||||
var c = Provider.of<CanvasModel>(context);
|
||||
final cursor = Provider.of<CursorModel>(context);
|
||||
final s = c.scale;
|
||||
if (c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
final imageWidget = SizedBox(
|
||||
@ -483,6 +493,8 @@ class ImagePaint extends StatelessWidget {
|
||||
child: CustomPaint(
|
||||
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
||||
));
|
||||
|
||||
Rx<Offset> pos = Rx<Offset>(const Offset(0.0, 0.0));
|
||||
return Center(
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
@ -498,9 +510,22 @@ class ImagePaint extends StatelessWidget {
|
||||
return false;
|
||||
},
|
||||
child: Obx(() => MouseRegion(
|
||||
cursor: (keyboardEnabled.isTrue && cursorOverImage.isTrue)
|
||||
? SystemMouseCursors.none
|
||||
cursor: (cursorOverImage.isTrue && keyboardEnabled.isTrue)
|
||||
? (remoteCursorMoved.isTrue
|
||||
? 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,
|
||||
onHover: (evt) {
|
||||
pos.value = evt.position;
|
||||
},
|
||||
child: _buildCrossScrollbar(_buildListener(imageWidget)))),
|
||||
),
|
||||
);
|
||||
|
@ -13,8 +13,10 @@ import '../../models/platform_model.dart';
|
||||
import '../../models/server_model.dart';
|
||||
|
||||
class DesktopServerPage extends StatefulWidget {
|
||||
const DesktopServerPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DesktopServerPageState();
|
||||
State<DesktopServerPage> createState() => _DesktopServerPageState();
|
||||
}
|
||||
|
||||
class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
|
@ -1031,6 +1031,7 @@ class PopupMenuButton<T> extends StatefulWidget {
|
||||
Key? key,
|
||||
required this.itemBuilder,
|
||||
this.initialValue,
|
||||
this.onHover,
|
||||
this.onSelected,
|
||||
this.onCanceled,
|
||||
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.
|
||||
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.
|
||||
///
|
||||
/// 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));
|
||||
|
||||
if (widget.child != null)
|
||||
if (widget.child != null) {
|
||||
return Tooltip(
|
||||
message:
|
||||
widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
|
||||
child: InkWell(
|
||||
onTap: widget.enabled ? showButtonMenu : null,
|
||||
onHover: widget.onHover,
|
||||
canRequestFocus: _canRequestFocus,
|
||||
radius: widget.splashRadius,
|
||||
enableFeedback: enableFeedback,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return IconButton(
|
||||
icon: widget.icon ?? Icon(Icons.adaptive.more),
|
||||
|
@ -427,7 +427,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
alignment: Alignment.centerRight,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Icon(Icons.edit),
|
||||
icon: const Icon(Icons.edit),
|
||||
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
|
||||
Future<MenuEntryBase<String>> _forceAlwaysRelayAction(String id) async {
|
||||
const option = 'force-always-relay';
|
||||
@ -620,11 +634,16 @@ class RecentPeerCard extends BasePeerCard {
|
||||
_transferFileAction(context, peer.id),
|
||||
_tcpTunnelingAction(context, peer.id),
|
||||
];
|
||||
MenuEntryBase<String>? rdpAction;
|
||||
if (peer.platform == 'Windows') {
|
||||
menuItems.add(_rdpAction(context, peer.id));
|
||||
rdpAction = _rdpAction(context, peer.id);
|
||||
}
|
||||
menuItems.add(MenuEntryDivider());
|
||||
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(_removeAction(peer.id, () async {
|
||||
await bind.mainLoadRecentPeers();
|
||||
@ -647,10 +666,16 @@ class FavoritePeerCard extends BasePeerCard {
|
||||
_transferFileAction(context, peer.id),
|
||||
_tcpTunnelingAction(context, peer.id),
|
||||
];
|
||||
MenuEntryBase<String>? rdpAction;
|
||||
if (peer.platform == 'Windows') {
|
||||
menuItems.add(_rdpAction(context, peer.id));
|
||||
rdpAction = _rdpAction(context, 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(_removeAction(peer.id, () async {
|
||||
await bind.mainLoadFavPeers();
|
||||
@ -673,10 +698,16 @@ class DiscoveredPeerCard extends BasePeerCard {
|
||||
_transferFileAction(context, peer.id),
|
||||
_tcpTunnelingAction(context, peer.id),
|
||||
];
|
||||
MenuEntryBase<String>? rdpAction;
|
||||
if (peer.platform == 'Windows') {
|
||||
menuItems.add(_rdpAction(context, peer.id));
|
||||
rdpAction = _rdpAction(context, 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(_removeAction(peer.id, () async {
|
||||
await bind.mainLoadLanPeers();
|
||||
@ -698,10 +729,16 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
_transferFileAction(context, peer.id),
|
||||
_tcpTunnelingAction(context, peer.id),
|
||||
];
|
||||
MenuEntryBase<String>? rdpAction;
|
||||
if (peer.platform == 'Windows') {
|
||||
menuItems.add(_rdpAction(context, peer.id));
|
||||
rdpAction = _rdpAction(context, 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(_removeAction(peer.id, () async {}));
|
||||
menuItems.add(_unrememberPasswordAction(peer.id));
|
||||
|
@ -12,7 +12,7 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
|
||||
key,
|
||||
this.height = kMinInteractiveDimension,
|
||||
this.padding,
|
||||
this.enable = true,
|
||||
this.enabled,
|
||||
this.textStyle,
|
||||
this.onTap,
|
||||
this.position = mod_menu.PopupMenuPosition.overSide,
|
||||
@ -25,7 +25,7 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
|
||||
final Offset offset;
|
||||
final TextStyle? textStyle;
|
||||
final EdgeInsets? padding;
|
||||
final bool enable;
|
||||
final RxBool? enabled;
|
||||
final void Function()? onTap;
|
||||
final List<mod_menu.PopupMenuEntry<T>> Function(BuildContext) itemBuilder;
|
||||
final Widget child;
|
||||
@ -56,25 +56,27 @@ class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
||||
TextStyle style = widget.textStyle ??
|
||||
popupMenuTheme.textStyle ??
|
||||
theme.textTheme.subtitle1!;
|
||||
|
||||
return mod_menu.PopupMenuButton<T>(
|
||||
enabled: widget.enable,
|
||||
position: widget.position,
|
||||
offset: widget.offset,
|
||||
onSelected: handleTap,
|
||||
itemBuilder: widget.itemBuilder,
|
||||
padding: EdgeInsets.zero,
|
||||
child: AnimatedDefaultTextStyle(
|
||||
style: style,
|
||||
duration: kThemeChangeDuration,
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(minHeight: widget.height),
|
||||
padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: widget.child,
|
||||
return Obx(() {
|
||||
return mod_menu.PopupMenuButton<T>(
|
||||
enabled: widget.enabled != null ? widget.enabled!.value : true,
|
||||
position: widget.position,
|
||||
offset: widget.offset,
|
||||
onSelected: handleTap,
|
||||
itemBuilder: widget.itemBuilder,
|
||||
padding: EdgeInsets.zero,
|
||||
child: AnimatedDefaultTextStyle(
|
||||
style: style,
|
||||
duration: kThemeChangeDuration,
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(minHeight: widget.height),
|
||||
padding:
|
||||
widget.padding ?? const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,8 +100,12 @@ class MenuConfig {
|
||||
|
||||
abstract class MenuEntryBase<T> {
|
||||
bool dismissOnClicked;
|
||||
RxBool? enabled;
|
||||
|
||||
MenuEntryBase({this.dismissOnClicked = false});
|
||||
MenuEntryBase({
|
||||
this.dismissOnClicked = false,
|
||||
this.enabled,
|
||||
});
|
||||
List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf);
|
||||
}
|
||||
|
||||
@ -119,9 +125,14 @@ class MenuEntryRadioOption {
|
||||
String text;
|
||||
String value;
|
||||
bool dismissOnClicked;
|
||||
RxBool? enabled;
|
||||
|
||||
MenuEntryRadioOption(
|
||||
{required this.text, required this.value, this.dismissOnClicked = false});
|
||||
MenuEntryRadioOption({
|
||||
required this.text,
|
||||
required this.value,
|
||||
this.dismissOnClicked = false,
|
||||
this.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
typedef RadioOptionsGetter = List<MenuEntryRadioOption> Function();
|
||||
@ -138,13 +149,14 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
|
||||
final RadioOptionSetter optionSetter;
|
||||
final RxString _curOption = "".obs;
|
||||
|
||||
MenuEntryRadios(
|
||||
{required this.text,
|
||||
required this.optionsGetter,
|
||||
required this.curOptionGetter,
|
||||
required this.optionSetter,
|
||||
dismissOnClicked = false})
|
||||
: super(dismissOnClicked: dismissOnClicked) {
|
||||
MenuEntryRadios({
|
||||
required this.text,
|
||||
required this.optionsGetter,
|
||||
required this.curOptionGetter,
|
||||
required this.optionSetter,
|
||||
dismissOnClicked = false,
|
||||
RxBool? enabled,
|
||||
}) : super(dismissOnClicked: dismissOnClicked, enabled: enabled) {
|
||||
() async {
|
||||
_curOption.value = await curOptionGetter();
|
||||
}();
|
||||
@ -220,13 +232,17 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
||||
final RadioOptionSetter optionSetter;
|
||||
final RxString _curOption = "".obs;
|
||||
|
||||
MenuEntrySubRadios(
|
||||
{required this.text,
|
||||
required this.optionsGetter,
|
||||
required this.curOptionGetter,
|
||||
required this.optionSetter,
|
||||
dismissOnClicked = false})
|
||||
: super(dismissOnClicked: dismissOnClicked) {
|
||||
MenuEntrySubRadios({
|
||||
required this.text,
|
||||
required this.optionsGetter,
|
||||
required this.curOptionGetter,
|
||||
required this.optionSetter,
|
||||
dismissOnClicked = false,
|
||||
RxBool? enabled,
|
||||
}) : super(
|
||||
dismissOnClicked: dismissOnClicked,
|
||||
enabled: enabled,
|
||||
) {
|
||||
() async {
|
||||
_curOption.value = await curOptionGetter();
|
||||
}();
|
||||
@ -293,6 +309,7 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
||||
BuildContext context, MenuConfig conf) {
|
||||
return [
|
||||
PopupMenuChildrenItem(
|
||||
enabled: super.enabled,
|
||||
padding: EdgeInsets.zero,
|
||||
height: conf.height,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
@ -325,9 +342,14 @@ typedef SwitchSetter = Future<void> Function(bool);
|
||||
|
||||
abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
||||
final String text;
|
||||
final Rx<TextStyle>? textStyle;
|
||||
|
||||
MenuEntrySwitchBase({required this.text, required dismissOnClicked})
|
||||
: super(dismissOnClicked: dismissOnClicked);
|
||||
MenuEntrySwitchBase({
|
||||
required this.text,
|
||||
required dismissOnClicked,
|
||||
this.textStyle,
|
||||
RxBool? enabled,
|
||||
}) : super(dismissOnClicked: dismissOnClicked, enabled: enabled);
|
||||
|
||||
RxBool get curOption;
|
||||
Future<void> setOption(bool option);
|
||||
@ -344,14 +366,23 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
height: conf.height,
|
||||
child: Row(children: [
|
||||
// const SizedBox(width: MenuConfig.midPadding),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
() {
|
||||
if (textStyle != null) {
|
||||
final style = textStyle!;
|
||||
return Obx(() => Text(
|
||||
text,
|
||||
style: style.value,
|
||||
));
|
||||
} else {
|
||||
return Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
);
|
||||
}
|
||||
}(),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
@ -384,12 +415,19 @@ class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
|
||||
final SwitchSetter setter;
|
||||
final RxBool _curOption = false.obs;
|
||||
|
||||
MenuEntrySwitch(
|
||||
{required String text,
|
||||
required this.getter,
|
||||
required this.setter,
|
||||
dismissOnClicked = false})
|
||||
: super(text: text, dismissOnClicked: dismissOnClicked) {
|
||||
MenuEntrySwitch({
|
||||
required String text,
|
||||
required this.getter,
|
||||
required this.setter,
|
||||
Rx<TextStyle>? textStyle,
|
||||
dismissOnClicked = false,
|
||||
RxBool? enabled,
|
||||
}) : super(
|
||||
text: text,
|
||||
textStyle: textStyle,
|
||||
dismissOnClicked: dismissOnClicked,
|
||||
enabled: enabled,
|
||||
) {
|
||||
() async {
|
||||
_curOption.value = await getter();
|
||||
}();
|
||||
@ -414,12 +452,17 @@ class MenuEntrySwitch2<T> extends MenuEntrySwitchBase<T> {
|
||||
final Switch2Getter getter;
|
||||
final SwitchSetter setter;
|
||||
|
||||
MenuEntrySwitch2(
|
||||
{required String text,
|
||||
required this.getter,
|
||||
required this.setter,
|
||||
dismissOnClicked = false})
|
||||
: super(text: text, dismissOnClicked: dismissOnClicked);
|
||||
MenuEntrySwitch2({
|
||||
required String text,
|
||||
required this.getter,
|
||||
required this.setter,
|
||||
Rx<TextStyle>? textStyle,
|
||||
dismissOnClicked = false,
|
||||
RxBool? enabled,
|
||||
}) : super(
|
||||
text: text,
|
||||
textStyle: textStyle,
|
||||
dismissOnClicked: dismissOnClicked);
|
||||
|
||||
@override
|
||||
RxBool get curOption => getter();
|
||||
@ -433,13 +476,18 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
final String text;
|
||||
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
|
||||
List<mod_menu.PopupMenuEntry<T>> build(
|
||||
BuildContext context, MenuConfig conf) {
|
||||
return [
|
||||
PopupMenuChildrenItem(
|
||||
enabled: super.enabled,
|
||||
height: conf.height,
|
||||
padding: EdgeInsets.zero,
|
||||
position: mod_menu.PopupMenuPosition.overSide,
|
||||
@ -449,20 +497,24 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
.toList(),
|
||||
child: Row(children: [
|
||||
const SizedBox(width: MenuConfig.midPadding),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
Obx(() => Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: (super.enabled != null ? super.enabled!.value : true)
|
||||
? Colors.black
|
||||
: Colors.grey,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
)),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
color: conf.commonColor,
|
||||
),
|
||||
child: Obx(() => Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
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;
|
||||
Function() proc;
|
||||
|
||||
MenuEntryButton(
|
||||
{required this.childBuilder,
|
||||
required this.proc,
|
||||
dismissOnClicked = false})
|
||||
: super(dismissOnClicked: dismissOnClicked);
|
||||
MenuEntryButton({
|
||||
required this.childBuilder,
|
||||
required this.proc,
|
||||
dismissOnClicked = false,
|
||||
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
|
||||
List<mod_menu.PopupMenuEntry<T>> build(
|
||||
BuildContext context, MenuConfig conf) {
|
||||
return [
|
||||
mod_menu.PopupMenuItem(
|
||||
enabled: super.enabled != null ? super.enabled!.value : true,
|
||||
padding: EdgeInsets.zero,
|
||||
height: conf.height,
|
||||
child: TextButton(
|
||||
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();
|
||||
},
|
||||
),
|
||||
child: _buildChild(context, conf),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
@ -75,20 +75,20 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
final List<Widget> menubarItems = [];
|
||||
if (!isWebDesktop) {
|
||||
menubarItems.add(_buildFullscreen(context));
|
||||
if (widget.ffi.ffiModel.isPeerAndroid) {
|
||||
menubarItems.add(IconButton(
|
||||
tooltip: translate('Mobile Actions'),
|
||||
color: _MenubarTheme.commonColor,
|
||||
icon: const Icon(Icons.build),
|
||||
onPressed: () {
|
||||
if (mobileActionsOverlayEntry == null) {
|
||||
showMobileActionsOverlay();
|
||||
} else {
|
||||
hideMobileActionsOverlay();
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
//if (widget.ffi.ffiModel.isPeerAndroid) {
|
||||
menubarItems.add(IconButton(
|
||||
tooltip: translate('Mobile Actions'),
|
||||
color: _MenubarTheme.commonColor,
|
||||
icon: const Icon(Icons.build),
|
||||
onPressed: () {
|
||||
if (mobileActionsOverlayEntry == null) {
|
||||
showMobileActionsOverlay();
|
||||
} else {
|
||||
hideMobileActionsOverlay();
|
||||
}
|
||||
},
|
||||
));
|
||||
//}
|
||||
}
|
||||
menubarItems.add(_buildMonitor(context));
|
||||
menubarItems.add(_buildControl(context));
|
||||
|
@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -54,7 +55,7 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
bool get touchMode => _touchMode;
|
||||
|
||||
bool get isPeerAndroid => _pi.platform == "Android";
|
||||
bool get isPeerAndroid => _pi.platform == 'Android';
|
||||
|
||||
set inputBlocked(v) {
|
||||
_inputBlocked = v;
|
||||
@ -116,7 +117,7 @@ class FfiModel with ChangeNotifier {
|
||||
return null;
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -143,17 +144,17 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'cursor_id') {
|
||||
parent.target?.cursorModel.updateCursorId(evt);
|
||||
} else if (name == 'cursor_position') {
|
||||
parent.target?.cursorModel.updateCursorPosition(evt);
|
||||
parent.target?.cursorModel.updateCursorPosition(evt, peerId);
|
||||
} 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'] ?? "");
|
||||
.receive(ChatModel.clientModeID, evt['text'] ?? '');
|
||||
} else if (name == 'chat_server_mode') {
|
||||
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') {
|
||||
parent.target?.fileModel.receiveFileDir(evt);
|
||||
} else if (name == 'job_progress') {
|
||||
@ -184,61 +185,7 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
/// Bind the event listener to receive events from the Rust core.
|
||||
void updateEventListener(String peerId) {
|
||||
cb(evt) {
|
||||
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);
|
||||
platformFFI.setEventCallback(startEventListener(peerId));
|
||||
}
|
||||
|
||||
void handleSwitchDisplay(Map<String, dynamic> evt) {
|
||||
@ -249,8 +196,9 @@ class FfiModel with ChangeNotifier {
|
||||
_display.y = double.parse(evt['y']);
|
||||
_display.width = int.parse(evt['width']);
|
||||
_display.height = int.parse(evt['height']);
|
||||
if (old != _pi.currentDisplay)
|
||||
if (old != _pi.currentDisplay) {
|
||||
parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y);
|
||||
}
|
||||
|
||||
// remote is mobile, and orientation changed
|
||||
if ((_display.width > _display.height) != oldOrientation) {
|
||||
@ -307,7 +255,7 @@ class FfiModel with ChangeNotifier {
|
||||
_pi.username = evt['username'];
|
||||
_pi.hostname = evt['hostname'];
|
||||
_pi.platform = evt['platform'];
|
||||
_pi.sasEnabled = evt['sas_enabled'] == "true";
|
||||
_pi.sasEnabled = evt['sas_enabled'] == 'true';
|
||||
_pi.currentDisplay = int.parse(evt['current_display']);
|
||||
|
||||
try {
|
||||
@ -323,7 +271,7 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
} else {
|
||||
_touchMode =
|
||||
await bind.sessionGetOption(id: peerId, arg: "touch-mode") != '';
|
||||
await bind.sessionGetOption(id: peerId, arg: 'touch-mode') != '';
|
||||
}
|
||||
|
||||
if (parent.target != null &&
|
||||
@ -381,7 +329,7 @@ class ImageModel with ChangeNotifier {
|
||||
|
||||
ui.Image? get image => _image;
|
||||
|
||||
String _id = "";
|
||||
String _id = '';
|
||||
|
||||
WeakReference<FFI> parent;
|
||||
|
||||
@ -426,7 +374,7 @@ class ImageModel with ChangeNotifier {
|
||||
}
|
||||
Future.delayed(Duration(milliseconds: 1), () {
|
||||
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();
|
||||
}
|
||||
});
|
||||
@ -471,7 +419,7 @@ class CanvasModel with ChangeNotifier {
|
||||
// the tabbar over the image
|
||||
double tabBarHeight = 0.0;
|
||||
// TODO multi canvas model
|
||||
String id = "";
|
||||
String id = '';
|
||||
// scroll offset x percent
|
||||
double _scrollX = 0.0;
|
||||
// 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 (parent.target != null) {
|
||||
if (parent.target!.ffiModel.keyboard()) {
|
||||
if (parent.target != null && parent.target!.ffiModel.keyboard()) {
|
||||
// Draw cursor if is not desktop.
|
||||
if (!isDesktop) {
|
||||
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 {
|
||||
ui.Image? _image;
|
||||
final _images = <int, Tuple3<ui.Image, double, double>>{};
|
||||
Uint8List? _pngData;
|
||||
final _pngs = <int, Uint8List?>{};
|
||||
double _x = -10000;
|
||||
double _y = -10000;
|
||||
double _hotx = 0;
|
||||
double _hoty = 0;
|
||||
double _displayOriginX = 0;
|
||||
double _displayOriginY = 0;
|
||||
String id = ""; // TODO multi cursor model
|
||||
String id = ''; // TODO multi cursor model
|
||||
WeakReference<FFI> parent;
|
||||
|
||||
ui.Image? get image => _image;
|
||||
Uint8List? get pngData => _pngData;
|
||||
|
||||
double get x => _x - _displayOriginX;
|
||||
|
||||
@ -801,19 +759,29 @@ class CursorModel with ChangeNotifier {
|
||||
var pid = parent.target?.id;
|
||||
ui.decodeImageFromPixels(rgba, width, height, ui.PixelFormat.rgba8888,
|
||||
(image) {
|
||||
if (parent.target?.id != pid) return;
|
||||
_image = image;
|
||||
_images[id] = Tuple3(image, _hotx, _hoty);
|
||||
try {
|
||||
// my throw exception, because the listener maybe already dispose
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('notify cursor: $e');
|
||||
}
|
||||
() async {
|
||||
if (parent.target?.id != pid) return;
|
||||
_image = image;
|
||||
_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 {
|
||||
// my throw exception, because the listener maybe already dispose
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('notify cursor: $e');
|
||||
}
|
||||
}();
|
||||
});
|
||||
}
|
||||
|
||||
void updateCursorId(Map<String, dynamic> evt) {
|
||||
_pngData = _pngs[int.parse(evt['id'])];
|
||||
final tmp = _images[int.parse(evt['id'])];
|
||||
if (tmp != null) {
|
||||
_image = tmp.item1;
|
||||
@ -824,9 +792,14 @@ class CursorModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Update the cursor position.
|
||||
void updateCursorPosition(Map<String, dynamic> evt) {
|
||||
void updateCursorPosition(Map<String, dynamic> evt, String id) {
|
||||
_x = double.parse(evt['x']);
|
||||
_y = double.parse(evt['y']);
|
||||
try {
|
||||
RemoteCursorMovedState.find(id).value = true;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@ -888,13 +861,15 @@ class QualityMonitorModel with ChangeNotifier {
|
||||
|
||||
updateQualityStatus(Map<String, dynamic> evt) {
|
||||
try {
|
||||
if ((evt["speed"] as String).isNotEmpty) _data.speed = evt["speed"];
|
||||
if ((evt["fps"] as String).isNotEmpty) _data.fps = evt["fps"];
|
||||
if ((evt["delay"] as String).isNotEmpty) _data.delay = evt["delay"];
|
||||
if ((evt["target_bitrate"] as String).isNotEmpty)
|
||||
_data.targetBitrate = evt["target_bitrate"];
|
||||
if ((evt["codec_format"] as String).isNotEmpty)
|
||||
_data.codecFormat = evt["codec_format"];
|
||||
if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed'];
|
||||
if ((evt['fps'] as String).isNotEmpty) _data.fps = evt['fps'];
|
||||
if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay'];
|
||||
if ((evt['target_bitrate'] as String).isNotEmpty) {
|
||||
_data.targetBitrate = evt['target_bitrate'];
|
||||
}
|
||||
if ((evt['codec_format'] as String).isNotEmpty) {
|
||||
_data.codecFormat = evt['codec_format'];
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (e) {}
|
||||
}
|
||||
@ -907,11 +882,11 @@ extension ToString on MouseButtons {
|
||||
String get value {
|
||||
switch (this) {
|
||||
case MouseButtons.left:
|
||||
return "left";
|
||||
return 'left';
|
||||
case MouseButtons.right:
|
||||
return "right";
|
||||
return 'right';
|
||||
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.
|
||||
class FFI {
|
||||
var id = "";
|
||||
var id = '';
|
||||
var shift = false;
|
||||
var ctrl = false;
|
||||
var alt = false;
|
||||
var command = false;
|
||||
var version = "";
|
||||
var version = '';
|
||||
var connType = ConnType.defaultConn;
|
||||
|
||||
/// dialogManager use late to ensure init after main page binding [globalKey]
|
||||
@ -1006,11 +981,11 @@ class FFI {
|
||||
// out['name'] = name;
|
||||
// // default: down = false
|
||||
// if (down == true) {
|
||||
// out['down'] = "true";
|
||||
// out['down'] = 'true';
|
||||
// }
|
||||
// // default: press = true
|
||||
// if (press != false) {
|
||||
// out['press'] = "true";
|
||||
// out['press'] = 'true';
|
||||
// }
|
||||
// setByName('input_key', json.encode(modify(out)));
|
||||
// TODO id
|
||||
@ -1038,7 +1013,7 @@ class FFI {
|
||||
Future<List<Peer>> peers() async {
|
||||
try {
|
||||
var str = await bind.mainGetRecentPeers();
|
||||
if (str == "") return [];
|
||||
if (str == '') return [];
|
||||
List<dynamic> peers = json.decode(str);
|
||||
return peers
|
||||
.map((s) => s as List<dynamic>)
|
||||
@ -1046,7 +1021,7 @@ class FFI {
|
||||
Peer.fromJson(s[0] as String, s[1] as Map<String, dynamic>))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
print('peers(): $e');
|
||||
debugPrint('peers(): $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@ -1056,7 +1031,7 @@ class FFI {
|
||||
{bool isFileTransfer = false,
|
||||
bool isPortForward = false,
|
||||
double tabBarHeight = 0.0}) {
|
||||
assert(!(isFileTransfer && isPortForward), "more than one connect type");
|
||||
assert(!(isFileTransfer && isPortForward), 'more than one connect type');
|
||||
if (isFileTransfer) {
|
||||
connType = ConnType.fileTransfer;
|
||||
id = 'ft_$id';
|
||||
@ -1108,13 +1083,13 @@ class FFI {
|
||||
canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay);
|
||||
}
|
||||
bind.sessionClose(id: id);
|
||||
id = "";
|
||||
id = '';
|
||||
imageModel.update(null, 0.0);
|
||||
cursorModel.clear();
|
||||
ffiModel.clear();
|
||||
canvasModel.clear();
|
||||
resetModifiers();
|
||||
debugPrint("model $id closed");
|
||||
debugPrint('model $id closed');
|
||||
}
|
||||
|
||||
/// Send **get** command to the Rust core based on [name] and [arg].
|
||||
@ -1221,7 +1196,7 @@ class FFI {
|
||||
Future<String> getDefaultAudioInput() async {
|
||||
final input = await bind.mainGetOption(key: 'audio-input');
|
||||
if (input.isEmpty && Platform.isWindows) {
|
||||
return "System Sound";
|
||||
return 'System Sound';
|
||||
}
|
||||
return input;
|
||||
}
|
||||
@ -1232,8 +1207,8 @@ class FFI {
|
||||
|
||||
Future<Map<String, String>> getHttpHeaders() async {
|
||||
return {
|
||||
"Authorization":
|
||||
"Bearer " + await bind.mainGetLocalOption(key: "access_token")
|
||||
'Authorization':
|
||||
'Bearer ' + await bind.mainGetLocalOption(key: 'access_token')
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1246,10 +1221,10 @@ class Display {
|
||||
}
|
||||
|
||||
class PeerInfo {
|
||||
String version = "";
|
||||
String username = "";
|
||||
String hostname = "";
|
||||
String platform = "";
|
||||
String version = '';
|
||||
String username = '';
|
||||
String hostname = '';
|
||||
String platform = '';
|
||||
bool sasEnabled = false;
|
||||
int currentDisplay = 0;
|
||||
List<Display> displays = [];
|
||||
|
@ -35,7 +35,7 @@ class ServerModel with ChangeNotifier {
|
||||
|
||||
final tabController = DesktopTabController(tabType: DesktopTabType.cm);
|
||||
|
||||
List<Client> _clients = [];
|
||||
final List<Client> _clients = [];
|
||||
|
||||
bool get isStart => _isStart;
|
||||
|
||||
@ -61,8 +61,8 @@ class ServerModel with ChangeNotifier {
|
||||
return _verificationMethod;
|
||||
}
|
||||
|
||||
set verificationMethod(String method) {
|
||||
bind.mainSetOption(key: "verification-method", value: method);
|
||||
setVerificationMethod(String method) async {
|
||||
await bind.mainSetOption(key: "verification-method", value: method);
|
||||
}
|
||||
|
||||
String get temporaryPasswordLength {
|
||||
@ -73,8 +73,8 @@ class ServerModel with ChangeNotifier {
|
||||
return _temporaryPasswordLength;
|
||||
}
|
||||
|
||||
set temporaryPasswordLength(String length) {
|
||||
bind.mainSetOption(key: "temporary-password-length", value: length);
|
||||
setTemporaryPasswordLength(String length) async {
|
||||
await bind.mainSetOption(key: "temporary-password-length", value: length);
|
||||
}
|
||||
|
||||
TextEditingController get serverId => _serverId;
|
||||
|
@ -68,6 +68,10 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/Kingtous/rustdesk_tray_manager
|
||||
ref: 3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a
|
||||
flutter_custom_cursor:
|
||||
git:
|
||||
url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor
|
||||
ref: 7fe78c139c711bafbae52d924e9caf18bd193e28
|
||||
get: ^4.6.5
|
||||
visibility_detector: ^0.3.3
|
||||
contextmenu: ^3.0.0
|
||||
|
@ -789,6 +789,10 @@ pub fn main_get_mouse_time() -> f64 {
|
||||
get_mouse_time()
|
||||
}
|
||||
|
||||
pub fn main_wol(id: String) {
|
||||
crate::lan::send_wol(id)
|
||||
}
|
||||
|
||||
pub fn cm_send_chat(conn_id: i32, msg: String) {
|
||||
crate::ui_cm_interface::send_chat(conn_id, msg);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user