mirror of
https://github.com/weyne85/rustdesk.git
synced 2025-10-29 17:00:05 +00:00
Merge remote-tracking branch 'rd/master' into feat/x11/clipboard-file/init
Signed-off-by: ClSlaid <cailue@bupt.edu.cn>
This commit is contained in:
@@ -14,6 +14,6 @@ import Flutter
|
||||
|
||||
public func dummyMethodToEnforceBundling() {
|
||||
dummy_method_to_enforce_bundling();
|
||||
session_get_rgba(nil);
|
||||
session_get_rgba(nil, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
||||
import 'package:flutter_hbb/main.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
@@ -593,7 +594,7 @@ closeConnection({String? id}) {
|
||||
}
|
||||
}
|
||||
|
||||
void windowOnTop(int? id) async {
|
||||
Future<void> windowOnTop(int? id) async {
|
||||
if (!isDesktop) {
|
||||
return;
|
||||
}
|
||||
@@ -1054,7 +1055,7 @@ Widget msgboxIcon(String type) {
|
||||
if (type == 'on-uac' || type == 'on-foreground-elevated') {
|
||||
iconData = Icons.admin_panel_settings;
|
||||
}
|
||||
if (type == "info") {
|
||||
if (type.contains('info')) {
|
||||
iconData = Icons.info;
|
||||
}
|
||||
if (iconData != null) {
|
||||
@@ -1834,10 +1835,10 @@ enum UriLinkType {
|
||||
// uri link handler
|
||||
bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
List<String>? args;
|
||||
if (cmdArgs != null) {
|
||||
if (cmdArgs != null && cmdArgs.isNotEmpty) {
|
||||
args = cmdArgs;
|
||||
// rustdesk <uri link>
|
||||
if (args.isNotEmpty && args[0].startsWith(kUniLinksPrefix)) {
|
||||
if (args[0].startsWith(kUniLinksPrefix)) {
|
||||
final uri = Uri.tryParse(args[0]);
|
||||
if (uri != null) {
|
||||
args = urlLinkToCmdArgs(uri);
|
||||
@@ -2317,7 +2318,7 @@ Widget dialogButton(String text,
|
||||
}
|
||||
}
|
||||
|
||||
int version_cmp(String v1, String v2) {
|
||||
int versionCmp(String v1, String v2) {
|
||||
return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2);
|
||||
}
|
||||
|
||||
@@ -2589,3 +2590,18 @@ String getDesktopTabLabel(String peerId, String alias) {
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
sessionRefreshVideo(SessionID sessionId, PeerInfo pi) async {
|
||||
if (pi.currentDisplay == kAllDisplayValue) {
|
||||
for (int i = 0; i < pi.displays.length; i++) {
|
||||
await bind.sessionRefresh(sessionId: sessionId, display: i);
|
||||
}
|
||||
} else {
|
||||
await bind.sessionRefresh(sessionId: sessionId, display: pi.currentDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
bool isChooseDisplayToOpenInNewWindow(PeerInfo pi, SessionID sessionId) =>
|
||||
pi.isSupportMultiDisplay &&
|
||||
useTextureRender &&
|
||||
bind.sessionGetDisplaysAsIndividualWindows(sessionId: sessionId) == 'Y';
|
||||
|
||||
@@ -229,6 +229,22 @@ class _AddressBookState extends State<AddressBook> {
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> filterMenuItem() {
|
||||
return MenuEntrySwitch<String>(
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate('Filter by intersection'),
|
||||
getter: () async {
|
||||
return filterAbTagByIntersection();
|
||||
},
|
||||
setter: (bool v) async {
|
||||
bind.mainSetLocalOption(key: filterAbTagOption, value: v ? 'Y' : '');
|
||||
gFFI.abModel.filterByIntersection.value = v;
|
||||
},
|
||||
dismissOnClicked: true,
|
||||
);
|
||||
}
|
||||
|
||||
void _showMenu(RelativeRect pos) {
|
||||
final items = [
|
||||
getEntry(translate("Add ID"), abAddId),
|
||||
@@ -236,6 +252,7 @@ class _AddressBookState extends State<AddressBook> {
|
||||
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
|
||||
sortMenuItem(),
|
||||
syncMenuItem(),
|
||||
filterMenuItem(),
|
||||
];
|
||||
|
||||
mod_menu.showMenu(
|
||||
|
||||
@@ -1295,7 +1295,7 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
|
||||
ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect;
|
||||
} catch (_) {}
|
||||
bool notShowFps = (await bind.mainIsUsingPublicServer() && direct != true) ||
|
||||
version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0;
|
||||
versionCmp(ffi.ffiModel.pi.version, '1.2.0') < 0;
|
||||
|
||||
final content = customImageQualityWidget(
|
||||
initQuality: qualityInitValue,
|
||||
|
||||
@@ -600,7 +600,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
await _openNewConnInAction(id, 'Open in New Tab', kOptionOpenInTabs);
|
||||
|
||||
_openInWindowsAction(String id) async => await _openNewConnInAction(
|
||||
id, 'Open in New Window', kOptionOpenInWindows);
|
||||
id, 'Open in new window', kOptionOpenInWindows);
|
||||
|
||||
_openNewConnInOptAction(String id) async =>
|
||||
mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
|
||||
|
||||
@@ -450,12 +450,21 @@ class AddressBookPeersView extends BasePeersView {
|
||||
if (selectedTags.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
for (final tag in selectedTags) {
|
||||
if (idents.contains(tag)) {
|
||||
return true;
|
||||
if (gFFI.abModel.filterByIntersection.value) {
|
||||
for (final tag in selectedTags) {
|
||||
if (!idents.contains(tag)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
for (final tag in selectedTags) {
|
||||
if (idents.contains(tag)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
bool isEditOsPassword = false;
|
||||
@@ -90,7 +91,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Row(children: [
|
||||
Text(translate(pi.is_headless ? 'OS Account' : 'OS Password')),
|
||||
Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')),
|
||||
Offstage(
|
||||
offstage: isDesktop,
|
||||
child: Icon(Icons.edit, color: MyTheme.accent).marginOnly(left: 12),
|
||||
@@ -99,13 +100,13 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
trailingIcon: Transform.scale(
|
||||
scale: 0.8,
|
||||
child: InkWell(
|
||||
onTap: () => pi.is_headless
|
||||
onTap: () => pi.isHeadless
|
||||
? showSetOSAccount(sessionId, ffi.dialogManager)
|
||||
: handleOsPasswordEditIcon(sessionId, ffi.dialogManager),
|
||||
child: Icon(Icons.edit),
|
||||
),
|
||||
),
|
||||
onPressed: () => pi.is_headless
|
||||
onPressed: () => pi.isHeadless
|
||||
? showSetOSAccount(sessionId, ffi.dialogManager)
|
||||
: handleOsPasswordAction(sessionId, ffi.dialogManager),
|
||||
),
|
||||
@@ -208,7 +209,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
ffiModel.keyboard &&
|
||||
pi.platform != kPeerPlatformAndroid &&
|
||||
pi.platform != kPeerPlatformMacOS &&
|
||||
version_cmp(pi.version, '1.2.0') >= 0) {
|
||||
versionCmp(pi.version, '1.2.0') >= 0 &&
|
||||
bind.peerGetDefaultSessionsCount(id: id) == 1) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Switch Sides')),
|
||||
onPressed: () =>
|
||||
@@ -217,8 +219,9 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
// refresh
|
||||
if (pi.version.isNotEmpty) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Refresh')),
|
||||
onPressed: () => bind.sessionRefresh(sessionId: sessionId)));
|
||||
child: Text(translate('Refresh')),
|
||||
onPressed: () => sessionRefreshVideo(sessionId, pi),
|
||||
));
|
||||
}
|
||||
// record
|
||||
var codecFormat = ffi.qualityMonitorModel.data.codecFormat;
|
||||
@@ -377,7 +380,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
// show remote cursor
|
||||
if (pi.platform != kPeerPlatformAndroid &&
|
||||
!ffi.canvasModel.cursorEmbedded &&
|
||||
!pi.is_wayland) {
|
||||
!pi.isWayland) {
|
||||
final state = ShowRemoteCursorState.find(id);
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
final option = 'show-remote-cursor';
|
||||
@@ -486,7 +489,8 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
value: rxValue.value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
if (ffiModel.pi.currentDisplay != 0) {
|
||||
if (ffiModel.pi.currentDisplay != 0 &&
|
||||
ffiModel.pi.currentDisplay != kAllDisplayValue) {
|
||||
msgBox(sessionId, 'custom-nook-nocancel-hasclose', 'info',
|
||||
'Please switch to Display 1 first', '', ffi.dialogManager);
|
||||
return;
|
||||
@@ -510,5 +514,24 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
},
|
||||
child: Text(translate('Swap control-command key'))));
|
||||
}
|
||||
|
||||
if (useTextureRender &&
|
||||
pi.isSupportMultiDisplay &&
|
||||
PrivacyModeState.find(id).isFalse &&
|
||||
pi.displaysCount.value > 1 &&
|
||||
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
|
||||
final value =
|
||||
bind.sessionGetDisplaysAsIndividualWindows(sessionId: ffi.sessionId) ==
|
||||
'Y';
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionSetDisplaysAsIndividualWindows(
|
||||
sessionId: sessionId, value: value ? 'Y' : '');
|
||||
},
|
||||
child: Text(translate('Show displays as individual windows'))));
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ const double kDesktopRemoteTabBarHeight = 28.0;
|
||||
const int kInvalidWindowId = -1;
|
||||
const int kMainWindowId = 0;
|
||||
|
||||
const kAllDisplayValue = -1;
|
||||
|
||||
const String kPeerPlatformWindows = "Windows";
|
||||
const String kPeerPlatformLinux = "Linux";
|
||||
const String kPeerPlatformMacOS = "Mac OS";
|
||||
@@ -37,11 +39,13 @@ const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
|
||||
const String kWindowEventNewFileTransfer = "new_file_transfer";
|
||||
const String kWindowEventNewPortForward = "new_port_forward";
|
||||
const String kWindowEventActiveSession = "active_session";
|
||||
const String kWindowEventActiveDisplaySession = "active_display_session";
|
||||
const String kWindowEventGetRemoteList = "get_remote_list";
|
||||
const String kWindowEventGetSessionIdList = "get_session_id_list";
|
||||
|
||||
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
|
||||
const String kWindowEventGetCachedSessionData = "get_cached_session_data";
|
||||
const String kWindowEventOpenMonitorSession = "open_monitor_session";
|
||||
|
||||
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
|
||||
const String kOptionOpenInTabs = "allow-open-in-tabs";
|
||||
@@ -60,6 +64,9 @@ const int kWindowMainId = 0;
|
||||
const String kPointerEventKindTouch = "touch";
|
||||
const String kPointerEventKindMouse = "mouse";
|
||||
|
||||
const String kKeyShowDisplaysAsIndividualWindows = 'displays_as_individual_windows';
|
||||
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
|
||||
|
||||
// the executable name of the portable version
|
||||
const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
|
||||
|
||||
|
||||
@@ -187,12 +187,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
? Theme.of(context).scaffoldBackgroundColor
|
||||
: Theme.of(context).colorScheme.background,
|
||||
child: Tooltip(
|
||||
message: translate('Settings'),
|
||||
child: Icon(
|
||||
Icons.more_vert_outlined,
|
||||
size: 20,
|
||||
color: hover.value ? textColor : textColor?.withOpacity(0.5),
|
||||
)),
|
||||
message: translate('Settings'),
|
||||
child: Icon(
|
||||
Icons.more_vert_outlined,
|
||||
size: 20,
|
||||
color: hover.value ? textColor : textColor?.withOpacity(0.5),
|
||||
)),
|
||||
),
|
||||
),
|
||||
onHover: (value) => hover.value = value,
|
||||
@@ -256,27 +256,27 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
child: Obx(() => RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: Tooltip(
|
||||
message: translate('Refresh Password'),
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: refreshHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
))
|
||||
)),
|
||||
message: translate('Refresh Password'),
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: refreshHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
)))),
|
||||
onHover: (value) => refreshHover.value = value,
|
||||
).marginOnly(right: 8, top: 4),
|
||||
InkWell(
|
||||
child: Obx(
|
||||
() => Tooltip(
|
||||
message: translate('Change Password'),
|
||||
child: Icon(
|
||||
Icons.edit,
|
||||
color:
|
||||
editHover.value ? textColor : Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
)).marginOnly(right: 8, top: 4),
|
||||
message: translate('Change Password'),
|
||||
child: Icon(
|
||||
Icons.edit,
|
||||
color: editHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
)).marginOnly(right: 8, top: 4),
|
||||
),
|
||||
onTap: () => DesktopSettingPage.switch2page(1),
|
||||
onHover: (value) => editHover.value = value,
|
||||
@@ -604,8 +604,17 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
debugPrint("Failed to parse window id '${call.arguments}': $e");
|
||||
}
|
||||
if (windowId != null) {
|
||||
await rustDeskWinManager.moveTabToNewWindow(windowId, args[1], args[2]);
|
||||
await rustDeskWinManager.moveTabToNewWindow(
|
||||
windowId, args[1], args[2]);
|
||||
}
|
||||
} else if (call.method == kWindowEventOpenMonitorSession) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final windowId = args['window_id'] as int;
|
||||
final peerId = args['peer_id'] as String;
|
||||
final display = args['display'] as int;
|
||||
final displayCount = args['display_count'] as int;
|
||||
await rustDeskWinManager.openMonitorSession(
|
||||
windowId, peerId, display, displayCount);
|
||||
}
|
||||
});
|
||||
_uniLinksSubscription = listenUniLinks();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
@@ -9,6 +10,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/plugin/manager.dart';
|
||||
@@ -322,6 +324,7 @@ class _GeneralState extends State<_General> {
|
||||
'enable-confirm-closing-tabs',
|
||||
isServer: false),
|
||||
_OptionCheckBox(context, 'Adaptive bitrate', 'enable-abr'),
|
||||
wallpaper(),
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Open connection in new tab',
|
||||
@@ -348,6 +351,42 @@ class _GeneralState extends State<_General> {
|
||||
return _Card(title: 'Other', children: children);
|
||||
}
|
||||
|
||||
Widget wallpaper() {
|
||||
return futureBuilder(future: () async {
|
||||
final support = await bind.mainSupportRemoveWallpaper();
|
||||
return support;
|
||||
}(), hasData: (data) {
|
||||
if (data is bool && data == true) {
|
||||
final option = 'allow-remove-wallpaper';
|
||||
bool value = mainGetBoolOptionSync(option);
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: _OptionCheckBox(
|
||||
context,
|
||||
'Remove wallpaper during incoming sessions',
|
||||
option,
|
||||
update: () {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
if (value)
|
||||
_CountDownButton(
|
||||
text: 'Test',
|
||||
second: 5,
|
||||
onPressed: () {
|
||||
bind.mainTestWallpaper(second: 5);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Offstage();
|
||||
});
|
||||
}
|
||||
|
||||
Widget hwcodec() {
|
||||
return Offstage(
|
||||
offstage: !bind.mainHasHwcodec(),
|
||||
@@ -729,11 +768,6 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
Widget more(BuildContext context) {
|
||||
bool enabled = !locked;
|
||||
return _Card(title: 'Security', children: [
|
||||
Offstage(
|
||||
offstage: !Platform.isWindows,
|
||||
child: _OptionCheckBox(context, 'Enable RDP', 'enable-rdp',
|
||||
enabled: enabled),
|
||||
),
|
||||
shareRdp(context, enabled),
|
||||
_OptionCheckBox(context, 'Deny LAN Discovery', 'enable-lan-discovery',
|
||||
reverse: true, enabled: enabled),
|
||||
@@ -1273,9 +1307,9 @@ class _DisplayState extends State<_Display> {
|
||||
}
|
||||
|
||||
Widget other(BuildContext context) {
|
||||
return _Card(title: 'Other Default Options', children: [
|
||||
final children = [
|
||||
otherRow('View Mode', 'view_only'),
|
||||
otherRow('show_monitors_tip', 'show_monitors_toolbar'),
|
||||
otherRow('show_monitors_tip', kKeyShowMonitorsToolbar),
|
||||
otherRow('Collapse toolbar', 'collapse_toolbar'),
|
||||
otherRow('Show remote cursor', 'show_remote_cursor'),
|
||||
otherRow('Zoom cursor', 'zoom-cursor'),
|
||||
@@ -1286,7 +1320,12 @@ class _DisplayState extends State<_Display> {
|
||||
otherRow('Lock after session end', 'lock_after_session_end'),
|
||||
otherRow('Privacy mode', 'privacy_mode'),
|
||||
otherRow('Reverse mouse wheel', 'reverse_mouse_wheel'),
|
||||
]);
|
||||
];
|
||||
if (useTextureRender) {
|
||||
children.add(otherRow('Show displays as individual windows',
|
||||
kKeyShowDisplaysAsIndividualWindows));
|
||||
}
|
||||
return _Card(title: 'Other Default Options', children: children);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1878,6 +1917,69 @@ class _ComboBox extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _CountDownButton extends StatefulWidget {
|
||||
_CountDownButton({
|
||||
Key? key,
|
||||
required this.text,
|
||||
required this.second,
|
||||
required this.onPressed,
|
||||
}) : super(key: key);
|
||||
final String text;
|
||||
final VoidCallback? onPressed;
|
||||
final int second;
|
||||
|
||||
@override
|
||||
State<_CountDownButton> createState() => _CountDownButtonState();
|
||||
}
|
||||
|
||||
class _CountDownButtonState extends State<_CountDownButton> {
|
||||
bool _isButtonDisabled = false;
|
||||
|
||||
late int _countdownSeconds = widget.second;
|
||||
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startCountdownTimer() {
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
if (_countdownSeconds <= 0) {
|
||||
setState(() {
|
||||
_isButtonDisabled = false;
|
||||
});
|
||||
timer.cancel();
|
||||
} else {
|
||||
setState(() {
|
||||
_countdownSeconds--;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: _isButtonDisabled
|
||||
? null
|
||||
: () {
|
||||
widget.onPressed?.call();
|
||||
setState(() {
|
||||
_isButtonDisabled = true;
|
||||
_countdownSeconds = widget.second;
|
||||
});
|
||||
_startCountdownTimer();
|
||||
},
|
||||
child: Text(
|
||||
_isButtonDisabled ? '$_countdownSeconds s' : translate(widget.text),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region dialogs
|
||||
|
||||
@@ -28,6 +28,7 @@ import '../widgets/tabbar_widget.dart';
|
||||
|
||||
final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
|
||||
|
||||
// Used to skip session close if "move to new window" is clicked.
|
||||
final Map<String, bool> closeSessionOnDispose = {};
|
||||
|
||||
class RemotePage extends StatefulWidget {
|
||||
@@ -36,6 +37,8 @@ class RemotePage extends StatefulWidget {
|
||||
required this.id,
|
||||
required this.sessionId,
|
||||
required this.tabWindowId,
|
||||
required this.display,
|
||||
required this.displays,
|
||||
required this.password,
|
||||
required this.toolbarState,
|
||||
required this.tabController,
|
||||
@@ -46,6 +49,8 @@ class RemotePage extends StatefulWidget {
|
||||
final String id;
|
||||
final SessionID? sessionId;
|
||||
final int? tabWindowId;
|
||||
final int? display;
|
||||
final List<int>? displays;
|
||||
final String? password;
|
||||
final ToolbarState toolbarState;
|
||||
final String? switchUuid;
|
||||
@@ -73,7 +78,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
late RxBool _zoomCursor;
|
||||
late RxBool _remoteCursorMoved;
|
||||
late RxBool _keyboardEnabled;
|
||||
late RenderTexture _renderTexture;
|
||||
final Map<int, RenderTexture> _renderTextures = {};
|
||||
|
||||
final _blockableOverlayState = BlockableOverlayState();
|
||||
|
||||
@@ -109,6 +114,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
switchUuid: widget.switchUuid,
|
||||
forceRelay: widget.forceRelay,
|
||||
tabWindowId: widget.tabWindowId,
|
||||
display: widget.display,
|
||||
displays: widget.displays,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
||||
@@ -118,9 +125,6 @@ class _RemotePageState extends State<RemotePage>
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.enable();
|
||||
}
|
||||
// Register texture.
|
||||
_renderTexture = RenderTexture();
|
||||
_renderTexture.create(sessionId);
|
||||
|
||||
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
||||
bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
||||
@@ -207,7 +211,9 @@ class _RemotePageState extends State<RemotePage>
|
||||
// https://github.com/flutter/flutter/issues/64935
|
||||
super.dispose();
|
||||
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
|
||||
await _renderTexture.destroy(closeSession);
|
||||
for (final texture in _renderTextures.values) {
|
||||
await texture.destroy(closeSession);
|
||||
}
|
||||
// ensure we leave this session, this is a double check
|
||||
_ffi.inputModel.enterOrLeave(false);
|
||||
DesktopMultiWindow.removeListener(this);
|
||||
@@ -245,6 +251,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
onEnterOrLeaveImageSetter: (func) =>
|
||||
_onEnterOrLeaveImage4Toolbar = func,
|
||||
onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null,
|
||||
setRemoteState: setState,
|
||||
);
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
@@ -392,6 +399,38 @@ class _RemotePageState extends State<RemotePage>
|
||||
);
|
||||
}
|
||||
|
||||
Map<int, RenderTexture> _updateGetRenderTextures(int curDisplay) {
|
||||
tryCreateTexture(int idx) {
|
||||
if (!_renderTextures.containsKey(idx)) {
|
||||
final renderTexture = RenderTexture();
|
||||
_renderTextures[idx] = renderTexture;
|
||||
renderTexture.create(idx, sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
tryRemoveTexture(int idx) {
|
||||
if (_renderTextures.containsKey(idx)) {
|
||||
_renderTextures[idx]!.destroy(true);
|
||||
_renderTextures.remove(idx);
|
||||
}
|
||||
}
|
||||
|
||||
if (curDisplay == kAllDisplayValue) {
|
||||
final displays = _ffi.ffiModel.pi.getCurDisplays();
|
||||
for (var i = 0; i < displays.length; i++) {
|
||||
tryCreateTexture(i);
|
||||
}
|
||||
} else {
|
||||
tryCreateTexture(curDisplay);
|
||||
for (var i = 0; i < _ffi.ffiModel.pi.displays.length; i++) {
|
||||
if (i != curDisplay) {
|
||||
tryRemoveTexture(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _renderTextures;
|
||||
}
|
||||
|
||||
Widget getBodyForDesktop(BuildContext context) {
|
||||
var paints = <Widget>[
|
||||
MouseRegion(onEnter: (evt) {
|
||||
@@ -402,16 +441,20 @@ class _RemotePageState extends State<RemotePage>
|
||||
Future.delayed(Duration.zero, () {
|
||||
Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
|
||||
});
|
||||
return ImagePaint(
|
||||
id: widget.id,
|
||||
zoomCursor: _zoomCursor,
|
||||
cursorOverImage: _cursorOverImage,
|
||||
keyboardEnabled: _keyboardEnabled,
|
||||
remoteCursorMoved: _remoteCursorMoved,
|
||||
textureId: _renderTexture.textureId,
|
||||
useTextureRender: RenderTexture.useTextureRender,
|
||||
listenerBuilder: (child) =>
|
||||
_buildRawTouchAndPointerRegion(child, enterView, leaveView),
|
||||
final peerDisplay = CurrentDisplayState.find(widget.id);
|
||||
return Obx(
|
||||
() => _ffi.ffiModel.pi.isSet.isFalse
|
||||
? Container(color: Colors.transparent)
|
||||
: Obx(() => ImagePaint(
|
||||
id: widget.id,
|
||||
zoomCursor: _zoomCursor,
|
||||
cursorOverImage: _cursorOverImage,
|
||||
keyboardEnabled: _keyboardEnabled,
|
||||
remoteCursorMoved: _remoteCursorMoved,
|
||||
renderTextures: _updateGetRenderTextures(peerDisplay.value),
|
||||
listenerBuilder: (child) => _buildRawTouchAndPointerRegion(
|
||||
child, enterView, leaveView),
|
||||
)),
|
||||
);
|
||||
}))
|
||||
];
|
||||
@@ -447,8 +490,7 @@ class ImagePaint extends StatefulWidget {
|
||||
final RxBool cursorOverImage;
|
||||
final RxBool keyboardEnabled;
|
||||
final RxBool remoteCursorMoved;
|
||||
final RxInt textureId;
|
||||
final bool useTextureRender;
|
||||
final Map<int, RenderTexture> renderTextures;
|
||||
final Widget Function(Widget)? listenerBuilder;
|
||||
|
||||
ImagePaint(
|
||||
@@ -458,8 +500,7 @@ class ImagePaint extends StatefulWidget {
|
||||
required this.cursorOverImage,
|
||||
required this.keyboardEnabled,
|
||||
required this.remoteCursorMoved,
|
||||
required this.textureId,
|
||||
required this.useTextureRender,
|
||||
required this.renderTextures,
|
||||
this.listenerBuilder})
|
||||
: super(key: key);
|
||||
|
||||
@@ -530,27 +571,13 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
});
|
||||
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
final imageWidth = c.getDisplayWidth() * s;
|
||||
final imageHeight = c.getDisplayHeight() * s;
|
||||
final imageSize = Size(imageWidth, imageHeight);
|
||||
late final Widget imageWidget;
|
||||
if (widget.useTextureRender) {
|
||||
imageWidget = SizedBox(
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
child: Obx(() => Texture(
|
||||
textureId: widget.textureId.value,
|
||||
filterQuality:
|
||||
isViewOriginal() ? FilterQuality.none : FilterQuality.low,
|
||||
)),
|
||||
);
|
||||
} else {
|
||||
imageWidget = CustomPaint(
|
||||
size: imageSize,
|
||||
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
||||
);
|
||||
}
|
||||
|
||||
final paintWidth = c.getDisplayWidth() * s;
|
||||
final paintHeight = c.getDisplayHeight() * s;
|
||||
final paintSize = Size(paintWidth, paintHeight);
|
||||
final paintWidget = useTextureRender
|
||||
? _BuildPaintTextureRender(
|
||||
c, s, Offset.zero, paintSize, isViewOriginal())
|
||||
: _buildScrollbarNonTextureRender(m, paintSize, s);
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
final percentX = _horizontal.hasClients
|
||||
@@ -570,43 +597,79 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
},
|
||||
child: mouseRegion(
|
||||
child: Obx(() => _buildCrossScrollbarFromLayout(
|
||||
context, _buildListener(imageWidget), c.size, imageSize)),
|
||||
context, _buildListener(paintWidget), c.size, paintSize)),
|
||||
));
|
||||
} else {
|
||||
late final Widget imageWidget;
|
||||
if (c.size.width > 0 && c.size.height > 0) {
|
||||
if (widget.useTextureRender) {
|
||||
final x = Platform.isLinux ? c.x.toInt().toDouble() : c.x;
|
||||
final y = Platform.isLinux ? c.y.toInt().toDouble() : c.y;
|
||||
imageWidget = Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: x,
|
||||
top: y,
|
||||
width: c.getDisplayWidth() * s,
|
||||
height: c.getDisplayHeight() * s,
|
||||
child: Texture(
|
||||
textureId: widget.textureId.value,
|
||||
filterQuality:
|
||||
isViewOriginal() ? FilterQuality.none : FilterQuality.low,
|
||||
final paintWidget = useTextureRender
|
||||
? _BuildPaintTextureRender(
|
||||
c,
|
||||
s,
|
||||
Offset(
|
||||
Platform.isLinux ? c.x.toInt().toDouble() : c.x,
|
||||
Platform.isLinux ? c.y.toInt().toDouble() : c.y,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
imageWidget = CustomPaint(
|
||||
size: Size(c.size.width, c.size.height),
|
||||
painter:
|
||||
ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
|
||||
);
|
||||
}
|
||||
return mouseRegion(child: _buildListener(imageWidget));
|
||||
c.size,
|
||||
isViewOriginal())
|
||||
: _buildScrollAuthNonTextureRender(m, c, s);
|
||||
return mouseRegion(child: _buildListener(paintWidget));
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildScrollbarNonTextureRender(
|
||||
ImageModel m, Size imageSize, double s) {
|
||||
return CustomPaint(
|
||||
size: imageSize,
|
||||
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScrollAuthNonTextureRender(
|
||||
ImageModel m, CanvasModel c, double s) {
|
||||
return CustomPaint(
|
||||
size: Size(c.size.width, c.size.height),
|
||||
painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _BuildPaintTextureRender(
|
||||
CanvasModel c, double s, Offset offset, Size size, bool isViewOriginal) {
|
||||
final ffiModel = c.parent.target!.ffiModel;
|
||||
final displays = ffiModel.pi.getCurDisplays();
|
||||
final children = <Widget>[];
|
||||
final rect = ffiModel.rect;
|
||||
if (rect == null) {
|
||||
return Container();
|
||||
}
|
||||
final curDisplay = ffiModel.pi.currentDisplay;
|
||||
for (var i = 0; i < displays.length; i++) {
|
||||
final textureId = widget
|
||||
.renderTextures[curDisplay == kAllDisplayValue ? i : curDisplay]
|
||||
?.textureId;
|
||||
if (textureId != null) {
|
||||
children.add(Positioned(
|
||||
left: (displays[i].x - rect.left) * s + offset.dx,
|
||||
top: (displays[i].y - rect.top) * s + offset.dy,
|
||||
width: displays[i].width * s,
|
||||
height: displays[i].height * s,
|
||||
child: Obx(() => Texture(
|
||||
textureId: textureId.value,
|
||||
filterQuality:
|
||||
isViewOriginal ? FilterQuality.none : FilterQuality.low,
|
||||
)),
|
||||
));
|
||||
}
|
||||
}
|
||||
return SizedBox(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
child: Stack(children: children),
|
||||
);
|
||||
}
|
||||
|
||||
MouseCursor _buildCursorOfCache(
|
||||
CursorModel cursor, double scale, CursorData? cache) {
|
||||
if (cache == null) {
|
||||
@@ -731,7 +794,11 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
);
|
||||
}
|
||||
|
||||
return widget;
|
||||
return Container(
|
||||
child: widget,
|
||||
width: layoutSize.width,
|
||||
height: layoutSize.height,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListener(Widget child) {
|
||||
@@ -770,9 +837,14 @@ class CursorPaint extends StatelessWidget {
|
||||
double cy = c.y;
|
||||
if (c.viewStyle.style == kRemoteViewStyleOriginal &&
|
||||
c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
final d = c.parent.target!.ffiModel.display;
|
||||
final imageWidth = d.width * c.scale;
|
||||
final imageHeight = d.height * c.scale;
|
||||
final rect = c.parent.target!.ffiModel.rect;
|
||||
if (rect == null) {
|
||||
// unreachable!
|
||||
debugPrint('unreachable! The displays rect is null.');
|
||||
return Container();
|
||||
}
|
||||
final imageWidth = rect.width * c.scale;
|
||||
final imageHeight = rect.height * c.scale;
|
||||
cx = -imageWidth * c.scrollX;
|
||||
cy = -imageHeight * c.scrollY;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
peerId = params['id'];
|
||||
final sessionId = params['session_id'];
|
||||
final tabWindowId = params['tab_window_id'];
|
||||
final display = params['display'];
|
||||
final displays = params['displays'];
|
||||
if (peerId != null) {
|
||||
ConnectionTypeState.init(peerId!);
|
||||
tabController.onSelected = (id) {
|
||||
@@ -80,6 +82,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
id: peerId!,
|
||||
sessionId: sessionId == null ? null : SessionID(sessionId),
|
||||
tabWindowId: tabWindowId,
|
||||
display: display,
|
||||
displays: displays?.cast<int>(),
|
||||
password: params['password'],
|
||||
toolbarState: _toolbarState,
|
||||
tabController: tabController,
|
||||
@@ -109,6 +113,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
final switchUuid = args['switch_uuid'];
|
||||
final sessionId = args['session_id'];
|
||||
final tabWindowId = args['tab_window_id'];
|
||||
final display = args['display'];
|
||||
final displays = args['displays'];
|
||||
windowOnTop(windowId());
|
||||
if (tabController.length == 0) {
|
||||
if (Platform.isMacOS && stateGlobal.closeOnFullscreen) {
|
||||
@@ -129,6 +135,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
id: id,
|
||||
sessionId: sessionId == null ? null : SessionID(sessionId),
|
||||
tabWindowId: tabWindowId,
|
||||
display: display,
|
||||
displays: displays?.cast<int>(),
|
||||
password: args['password'],
|
||||
toolbarState: _toolbarState,
|
||||
tabController: tabController,
|
||||
@@ -148,6 +156,15 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
windowOnTop(windowId());
|
||||
}
|
||||
return jumpOk;
|
||||
} else if (call.method == kWindowEventActiveDisplaySession) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final display = args['display'];
|
||||
final jumpOk = tabController.jumpToByKeyAndDisplay(id, display);
|
||||
if (jumpOk) {
|
||||
windowOnTop(windowId());
|
||||
}
|
||||
return jumpOk;
|
||||
} else if (call.method == kWindowEventGetRemoteList) {
|
||||
return tabController.state.value.tabs
|
||||
.map((e) => e.key)
|
||||
@@ -160,18 +177,20 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
.join(';');
|
||||
} else if (call.method == kWindowEventGetCachedSessionData) {
|
||||
// Ready to show new window and close old tab.
|
||||
final peerId = call.arguments;
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final close = args['close'];
|
||||
try {
|
||||
final remotePage = tabController.state.value.tabs
|
||||
.firstWhere((tab) => tab.key == peerId)
|
||||
.firstWhere((tab) => tab.key == id)
|
||||
.page as RemotePage;
|
||||
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
|
||||
} catch (e) {
|
||||
debugPrint('Failed to get cached session data: $e');
|
||||
}
|
||||
if (returnValue != null) {
|
||||
closeSessionOnDispose[peerId] = false;
|
||||
tabController.closeBy(peerId);
|
||||
if (close && returnValue != null) {
|
||||
closeSessionOnDispose[id] = false;
|
||||
tabController.closeBy(id);
|
||||
}
|
||||
}
|
||||
_update_remote_count();
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
||||
import 'package:flutter_hbb/main.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
||||
@@ -325,7 +328,8 @@ class RemoteToolbar extends StatefulWidget {
|
||||
final FFI ffi;
|
||||
final ToolbarState state;
|
||||
final Function(Function(bool)) onEnterOrLeaveImageSetter;
|
||||
final Function() onEnterOrLeaveImageCleaner;
|
||||
final VoidCallback onEnterOrLeaveImageCleaner;
|
||||
final Function(VoidCallback) setRemoteState;
|
||||
|
||||
RemoteToolbar({
|
||||
Key? key,
|
||||
@@ -334,6 +338,7 @@ class RemoteToolbar extends StatefulWidget {
|
||||
required this.state,
|
||||
required this.onEnterOrLeaveImageSetter,
|
||||
required this.onEnterOrLeaveImageCleaner,
|
||||
required this.setRemoteState,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -450,13 +455,17 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
|
||||
}
|
||||
|
||||
if (PrivacyModeState.find(widget.id).isFalse && pi.displays.length > 1) {
|
||||
toolbarItems.add(
|
||||
bind.mainGetUserDefaultOption(key: 'show_monitors_toolbar') == 'Y'
|
||||
? _MultiMonitorMenu(id: widget.id, ffi: widget.ffi)
|
||||
: _MonitorMenu(id: widget.id, ffi: widget.ffi),
|
||||
);
|
||||
}
|
||||
toolbarItems.add(Obx(() {
|
||||
if (PrivacyModeState.find(widget.id).isFalse &&
|
||||
pi.displaysCount.value > 1) {
|
||||
return _MonitorMenu(
|
||||
id: widget.id,
|
||||
ffi: widget.ffi,
|
||||
setRemoteState: widget.setRemoteState);
|
||||
} else {
|
||||
return Offstage();
|
||||
}
|
||||
}));
|
||||
|
||||
toolbarItems
|
||||
.add(_ControlMenu(id: widget.id, ffi: widget.ffi, state: widget.state));
|
||||
@@ -581,11 +590,25 @@ class _MobileActionMenu extends StatelessWidget {
|
||||
class _MonitorMenu extends StatelessWidget {
|
||||
final String id;
|
||||
final FFI ffi;
|
||||
const _MonitorMenu({Key? key, required this.id, required this.ffi})
|
||||
: super(key: key);
|
||||
final Function(VoidCallback) setRemoteState;
|
||||
const _MonitorMenu({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.ffi,
|
||||
required this.setRemoteState,
|
||||
}) : super(key: key);
|
||||
|
||||
bool get showMonitorsToolbar =>
|
||||
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y';
|
||||
|
||||
bool get supportIndividualWindows =>
|
||||
useTextureRender && ffi.ffiModel.pi.isSupportMultiDisplay;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context) =>
|
||||
showMonitorsToolbar ? buildMultiMonitorMenu() : buildMonitorMenu();
|
||||
|
||||
Widget buildMonitorMenu() {
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Select Monitor',
|
||||
icon: icon(),
|
||||
@@ -595,7 +618,106 @@ class _MonitorMenu extends StatelessWidget {
|
||||
menuStyle: MenuStyle(
|
||||
padding:
|
||||
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
|
||||
menuChildren: [Row(children: displays(context))]);
|
||||
menuChildren: [buildMonitorSubmenuWidget()]);
|
||||
}
|
||||
|
||||
Widget buildMultiMonitorMenu() {
|
||||
return Row(children: buildMonitorList(true));
|
||||
}
|
||||
|
||||
Widget buildMonitorSubmenuWidget() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(children: buildMonitorList(false)),
|
||||
supportIndividualWindows ? Divider() : Offstage(),
|
||||
supportIndividualWindows ? chooseDisplayBehavior() : Offstage(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget chooseDisplayBehavior() {
|
||||
final value =
|
||||
bind.sessionGetDisplaysAsIndividualWindows(sessionId: ffi.sessionId) ==
|
||||
'Y';
|
||||
return CkbMenuButton(
|
||||
value: value,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionSetDisplaysAsIndividualWindows(
|
||||
sessionId: ffi.sessionId, value: value ? 'Y' : '');
|
||||
},
|
||||
ffi: ffi,
|
||||
child: Text(translate('Show displays as individual windows')));
|
||||
}
|
||||
|
||||
List<Widget> buildMonitorList(bool isMulti) {
|
||||
final List<Widget> monitorList = [];
|
||||
final pi = ffi.ffiModel.pi;
|
||||
|
||||
getMonitorText(int i) {
|
||||
if (i == kAllDisplayValue) {
|
||||
if (pi.displays.length == 2) {
|
||||
return '1|2';
|
||||
} else {
|
||||
return 'ALL';
|
||||
}
|
||||
} else {
|
||||
return (i + 1).toString();
|
||||
}
|
||||
}
|
||||
|
||||
buildMonitorButton(int i) => Obx(() {
|
||||
RxInt display = CurrentDisplayState.find(id);
|
||||
return _IconMenuButton(
|
||||
tooltip: isMulti ? '' : '#${i + 1} monitor',
|
||||
hMargin: isMulti ? null : 6,
|
||||
vMargin: isMulti ? null : 12,
|
||||
topLevel: false,
|
||||
color: i == display.value
|
||||
? _ToolbarTheme.blueColor
|
||||
: _ToolbarTheme.inactiveColor,
|
||||
hoverColor: i == display.value
|
||||
? _ToolbarTheme.hoverBlueColor
|
||||
: _ToolbarTheme.hoverInactiveColor,
|
||||
icon: Container(
|
||||
alignment: AlignmentDirectional.center,
|
||||
constraints:
|
||||
const BoxConstraints(minHeight: _ToolbarTheme.height),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
"assets/screen.svg",
|
||||
colorFilter:
|
||||
ColorFilter.mode(Colors.white, BlendMode.srcIn),
|
||||
),
|
||||
Obx(
|
||||
() => Text(
|
||||
getMonitorText(i),
|
||||
style: TextStyle(
|
||||
color: i == display.value
|
||||
? _ToolbarTheme.blueColor
|
||||
: _ToolbarTheme.inactiveColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onPressed: () => onPressed(i, pi),
|
||||
);
|
||||
});
|
||||
|
||||
for (int i = 0; i < pi.displays.length; i++) {
|
||||
monitorList.add(buildMonitorButton(i));
|
||||
}
|
||||
if (supportIndividualWindows && pi.displays.length > 1) {
|
||||
monitorList.add(buildMonitorButton(kAllDisplayValue));
|
||||
}
|
||||
return monitorList;
|
||||
}
|
||||
|
||||
icon() {
|
||||
@@ -610,7 +732,7 @@ class _MonitorMenu extends StatelessWidget {
|
||||
Obx(() {
|
||||
RxInt display = CurrentDisplayState.find(id);
|
||||
return Text(
|
||||
'${display.value + 1}/${pi.displays.length}',
|
||||
'${display.value == kAllDisplayValue ? 'A' : '${display.value + 1}'}/${pi.displays.length}',
|
||||
style: const TextStyle(
|
||||
color: _ToolbarTheme.blueColor,
|
||||
fontSize: 8,
|
||||
@@ -622,48 +744,44 @@ class _MonitorMenu extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> displays(BuildContext context) {
|
||||
final List<Widget> rowChildren = [];
|
||||
final pi = ffi.ffiModel.pi;
|
||||
for (int i = 0; i < pi.displays.length; i++) {
|
||||
rowChildren.add(_IconMenuButton(
|
||||
topLevel: false,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
tooltip: "#${i + 1} monitor",
|
||||
hMargin: 6,
|
||||
vMargin: 12,
|
||||
icon: Container(
|
||||
alignment: AlignmentDirectional.center,
|
||||
constraints: const BoxConstraints(minHeight: _ToolbarTheme.height),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
"assets/screen.svg",
|
||||
colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
|
||||
),
|
||||
Text(
|
||||
(i + 1).toString(),
|
||||
style: TextStyle(
|
||||
color: _ToolbarTheme.blueColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
_menuDismissCallback(ffi);
|
||||
RxInt display = CurrentDisplayState.find(id);
|
||||
if (display.value != i) {
|
||||
bind.sessionSwitchDisplay(sessionId: ffi.sessionId, value: i);
|
||||
}
|
||||
},
|
||||
));
|
||||
// Open new tab or window to show this monitor.
|
||||
// For now just open new window.
|
||||
openMonitorInNewTabOrWindow(int i, PeerInfo pi) {
|
||||
if (kWindowId == null) {
|
||||
// unreachable
|
||||
debugPrint('openMonitorInNewTabOrWindow, unreachable! kWindowId is null');
|
||||
return;
|
||||
}
|
||||
DesktopMultiWindow.invokeMethod(
|
||||
kMainWindowId,
|
||||
kWindowEventOpenMonitorSession,
|
||||
jsonEncode({
|
||||
'window_id': kWindowId!,
|
||||
'peer_id': ffi.id,
|
||||
'display': i,
|
||||
'display_count': pi.displays.length,
|
||||
}));
|
||||
}
|
||||
|
||||
openMonitorInTheSameTab(int i, PeerInfo pi) {
|
||||
final displays = i == kAllDisplayValue
|
||||
? List.generate(pi.displays.length, (index) => index)
|
||||
: [i];
|
||||
bind.sessionSwitchDisplay(
|
||||
sessionId: ffi.sessionId, value: Int32List.fromList(displays));
|
||||
ffi.ffiModel.switchToNewDisplay(i, ffi.sessionId, id);
|
||||
}
|
||||
|
||||
onPressed(int i, PeerInfo pi) {
|
||||
_menuDismissCallback(ffi);
|
||||
RxInt display = CurrentDisplayState.find(id);
|
||||
if (display.value != i) {
|
||||
if (isChooseDisplayToOpenInNewWindow(pi, ffi.sessionId)) {
|
||||
openMonitorInNewTabOrWindow(i, pi);
|
||||
} else {
|
||||
openMonitorInTheSameTab(i, pi);
|
||||
}
|
||||
}
|
||||
return rowChildren;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1044,14 +1162,14 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
Resolution? _localResolution;
|
||||
|
||||
late final TextEditingController _customWidth =
|
||||
TextEditingController(text: display.width.toString());
|
||||
TextEditingController(text: rect?.width.toInt().toString() ?? '');
|
||||
late final TextEditingController _customHeight =
|
||||
TextEditingController(text: display.height.toString());
|
||||
TextEditingController(text: rect?.height.toInt().toString() ?? '');
|
||||
|
||||
FFI get ffi => widget.ffi;
|
||||
PeerInfo get pi => widget.ffi.ffiModel.pi;
|
||||
FfiModel get ffiModel => widget.ffi.ffiModel;
|
||||
Display get display => ffiModel.display;
|
||||
Rect? get rect => ffiModel.rect;
|
||||
List<Resolution> get resolutions => pi.resolutions;
|
||||
bool get isWayland => bind.mainCurrentIsWayland();
|
||||
|
||||
@@ -1063,12 +1181,12 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isVirtualDisplay = display.isVirtualDisplayResolution;
|
||||
final isVirtualDisplay = ffiModel.isVirtualDisplayResolution;
|
||||
final visible =
|
||||
ffiModel.keyboard && (isVirtualDisplay || resolutions.length > 1);
|
||||
if (!visible) return Offstage();
|
||||
final showOriginalBtn =
|
||||
display.isOriginalResolutionSet && !display.isOriginalResolution;
|
||||
ffiModel.isOriginalResolutionSet && !ffiModel.isOriginalResolution;
|
||||
final showFitLocalBtn = !_isRemoteResolutionFitLocal();
|
||||
_setGroupValue();
|
||||
return _SubmenuButton(
|
||||
@@ -1085,12 +1203,15 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
}
|
||||
|
||||
_setGroupValue() {
|
||||
if (pi.currentDisplay == kAllDisplayValue) {
|
||||
return;
|
||||
}
|
||||
final lastGroupValue =
|
||||
stateGlobal.getLastResolutionGroupValue(widget.id, pi.currentDisplay);
|
||||
if (lastGroupValue == _kCustomResolutionValue) {
|
||||
_groupValue = _kCustomResolutionValue;
|
||||
} else {
|
||||
_groupValue = '${display.width}x${display.height}';
|
||||
_groupValue = '${rect?.width.toInt()}x${rect?.height.toInt()}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1118,20 +1239,23 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
|
||||
_getLocalResolution() {
|
||||
_localResolution = null;
|
||||
final String currentDisplay = bind.mainGetCurrentDisplay();
|
||||
if (currentDisplay.isNotEmpty) {
|
||||
final String mainDisplay = bind.mainGetMainDisplay();
|
||||
if (mainDisplay.isNotEmpty) {
|
||||
try {
|
||||
final display = json.decode(currentDisplay);
|
||||
final display = json.decode(mainDisplay);
|
||||
if (display['w'] != null && display['h'] != null) {
|
||||
_localResolution = Resolution(display['w'], display['h']);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Failed to decode $currentDisplay, $e');
|
||||
debugPrint('Failed to decode $mainDisplay, $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onChanged(BuildContext context, String? value) async {
|
||||
if (pi.currentDisplay == kAllDisplayValue) {
|
||||
return;
|
||||
}
|
||||
stateGlobal.setLastResolutionGroupValue(
|
||||
widget.id, pi.currentDisplay, value);
|
||||
if (value == null) return;
|
||||
@@ -1150,13 +1274,16 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
}
|
||||
|
||||
if (w != null && h != null) {
|
||||
if (w != display.width || h != display.height) {
|
||||
if (w != rect?.width.toInt() || h != rect?.height.toInt()) {
|
||||
await _changeResolution(context, w, h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_changeResolution(BuildContext context, int w, int h) async {
|
||||
if (pi.currentDisplay == kAllDisplayValue) {
|
||||
return;
|
||||
}
|
||||
await bind.sessionChangeResolution(
|
||||
sessionId: ffi.sessionId,
|
||||
display: pi.currentDisplay,
|
||||
@@ -1164,8 +1291,11 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
height: h,
|
||||
);
|
||||
Future.delayed(Duration(seconds: 3), () async {
|
||||
final display = ffiModel.display;
|
||||
if (w == display.width && h == display.height) {
|
||||
final rect = ffiModel.rect;
|
||||
if (rect == null) {
|
||||
return;
|
||||
}
|
||||
if (w == rect.width.toInt() && h == rect.height.toInt()) {
|
||||
if (await widget.screenAdjustor.isWindowCanBeAdjusted()) {
|
||||
widget.screenAdjustor.doAdjustWindow(context);
|
||||
}
|
||||
@@ -1175,6 +1305,10 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
|
||||
Widget _OriginalResolutionMenuButton(
|
||||
BuildContext context, bool showOriginalBtn) {
|
||||
final display = pi.tryGetDisplayIfNotAllDisplay();
|
||||
if (display == null) {
|
||||
return Offstage();
|
||||
}
|
||||
return Offstage(
|
||||
offstage: !showOriginalBtn,
|
||||
child: MenuButton(
|
||||
@@ -1262,7 +1396,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (display.isVirtualDisplayResolution) {
|
||||
if (ffiModel.isVirtualDisplayResolution) {
|
||||
return _localResolution!;
|
||||
}
|
||||
|
||||
@@ -1284,8 +1418,8 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
if (bestFitResolution == null) {
|
||||
return true;
|
||||
}
|
||||
return bestFitResolution.width == display.width &&
|
||||
bestFitResolution.height == display.height;
|
||||
return bestFitResolution.width == rect?.width.toInt() &&
|
||||
bestFitResolution.height == rect?.height.toInt();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1361,7 +1495,7 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pi.is_wayland && mode.key != _kKeyMapMode) {
|
||||
if (pi.isWayland && mode.key != _kKeyMapMode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1404,7 +1538,7 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
|
||||
viewMode() {
|
||||
final ffiModel = ffi.ffiModel;
|
||||
final enabled = version_cmp(pi.version, '1.2.0') >= 0 && ffiModel.keyboard;
|
||||
final enabled = versionCmp(pi.version, '1.2.0') >= 0 && ffiModel.keyboard;
|
||||
return CkbMenuButton(
|
||||
value: ffiModel.viewOnly,
|
||||
onChanged: enabled
|
||||
@@ -2037,71 +2171,3 @@ Widget _buildPointerTrackWidget(Widget child, FFI ffi) {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _MultiMonitorMenu extends StatelessWidget {
|
||||
final String id;
|
||||
final FFI ffi;
|
||||
|
||||
const _MultiMonitorMenu({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.ffi,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Widget> rowChildren = [];
|
||||
final pi = ffi.ffiModel.pi;
|
||||
|
||||
for (int i = 0; i < pi.displays.length; i++) {
|
||||
rowChildren.add(
|
||||
Obx(() {
|
||||
RxInt display = CurrentDisplayState.find(id);
|
||||
return _IconMenuButton(
|
||||
tooltip: "",
|
||||
topLevel: false,
|
||||
color: i == display.value
|
||||
? _ToolbarTheme.blueColor
|
||||
: Colors.grey[800]!,
|
||||
hoverColor: i == display.value
|
||||
? _ToolbarTheme.hoverBlueColor
|
||||
: Colors.grey[850]!,
|
||||
icon: Container(
|
||||
alignment: AlignmentDirectional.center,
|
||||
constraints:
|
||||
const BoxConstraints(minHeight: _ToolbarTheme.height),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
"assets/screen.svg",
|
||||
colorFilter:
|
||||
ColorFilter.mode(Colors.white, BlendMode.srcIn),
|
||||
),
|
||||
Obx(
|
||||
() => Text(
|
||||
(i + 1).toString(),
|
||||
style: TextStyle(
|
||||
color: i == display.value
|
||||
? _ToolbarTheme.blueColor
|
||||
: Colors.grey[800]!,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
if (display.value != i) {
|
||||
bind.sessionSwitchDisplay(sessionId: ffi.sessionId, value: i);
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Row(children: rowChildren);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart' hide TabBarTheme;
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
||||
import 'package:flutter_hbb/main.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
@@ -176,6 +177,19 @@ class DesktopTabController {
|
||||
jumpTo(state.value.tabs.indexWhere((tab) => tab.key == key),
|
||||
callOnSelected: callOnSelected);
|
||||
|
||||
bool jumpToByKeyAndDisplay(String key, int display) {
|
||||
for (int i = 0; i < state.value.tabs.length; i++) {
|
||||
final tab = state.value.tabs[i];
|
||||
if (tab.key == key) {
|
||||
final ffi = (tab.page as RemotePage).ffi;
|
||||
if (ffi.ffiModel.pi.currentDisplay == display) {
|
||||
return jumpTo(i, callOnSelected: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void closeBy(String? key) {
|
||||
if (!isDesktop) return;
|
||||
assert(onRemoved != null);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -755,14 +756,14 @@ void showOptions(
|
||||
if (image != null) {
|
||||
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
|
||||
}
|
||||
if (pi.displays.length > 1) {
|
||||
if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) {
|
||||
final cur = pi.currentDisplay;
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < pi.displays.length; ++i) {
|
||||
children.add(InkWell(
|
||||
onTap: () {
|
||||
if (i == cur) return;
|
||||
bind.sessionSwitchDisplay(sessionId: gFFI.sessionId, value: i);
|
||||
bind.sessionSwitchDisplay(sessionId: gFFI.sessionId, value: Int32List.fromList([i]));
|
||||
gFFI.dialogManager.dismissAll();
|
||||
},
|
||||
child: Ink(
|
||||
|
||||
@@ -328,13 +328,20 @@ class _ScamWarningDialogState extends State<ScamWarningDialog> {
|
||||
),
|
||||
),
|
||||
SizedBox(height: 18),
|
||||
Text(
|
||||
translate("scam_text1")+"\n\n"
|
||||
+translate("scam_text2")+"\n",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16.0,
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
translate("scam_text1")+"\n\n"
|
||||
+translate("scam_text2")+"\n",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
@@ -361,7 +368,9 @@ class _ScamWarningDialogState extends State<ScamWarningDialog> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: 150),
|
||||
child: ElevatedButton(
|
||||
onPressed: isButtonLocked
|
||||
? null
|
||||
: () {
|
||||
@@ -380,10 +389,15 @@ class _ScamWarningDialogState extends State<ScamWarningDialog> {
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13.0,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 15),
|
||||
ElevatedButton(
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: 150),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
@@ -396,8 +410,11 @@ class _ScamWarningDialogState extends State<ScamWarningDialog> {
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13.0,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)])),
|
||||
contentPadding: EdgeInsets.all(0.0),
|
||||
|
||||
@@ -23,6 +23,11 @@ bool shouldSortTags() {
|
||||
return bind.mainGetLocalOption(key: sortAbTagsOption).isNotEmpty;
|
||||
}
|
||||
|
||||
final filterAbTagOption = 'filter-ab-by-intersection';
|
||||
bool filterAbTagByIntersection() {
|
||||
return bind.mainGetLocalOption(key: filterAbTagOption).isNotEmpty;
|
||||
}
|
||||
|
||||
class AbModel {
|
||||
final abLoading = false.obs;
|
||||
final pullError = "".obs;
|
||||
@@ -31,6 +36,7 @@ class AbModel {
|
||||
final RxMap<String, int> tagColors = Map<String, int>.fromEntries([]).obs;
|
||||
final peers = List<Peer>.empty(growable: true).obs;
|
||||
final sortTags = shouldSortTags().obs;
|
||||
final filterByIntersection = filterAbTagByIntersection().obs;
|
||||
final retrying = false.obs;
|
||||
bool get emtpy => peers.isEmpty && tags.isEmpty;
|
||||
|
||||
|
||||
@@ -4,25 +4,30 @@ import 'package:texture_rgba_renderer/texture_rgba_renderer.dart';
|
||||
import '../../common.dart';
|
||||
import './platform_model.dart';
|
||||
|
||||
final useTextureRender = bind.mainUseTextureRender();
|
||||
|
||||
class RenderTexture {
|
||||
final RxInt textureId = RxInt(-1);
|
||||
int _textureKey = -1;
|
||||
int _display = 0;
|
||||
SessionID? _sessionId;
|
||||
static final useTextureRender = bind.mainUseTextureRender();
|
||||
|
||||
final textureRenderer = TextureRgbaRenderer();
|
||||
|
||||
RenderTexture();
|
||||
|
||||
create(SessionID sessionId) {
|
||||
int get display => _display;
|
||||
|
||||
create(int d, SessionID sessionId) {
|
||||
if (useTextureRender) {
|
||||
_display = d;
|
||||
_textureKey = bind.getNextTextureKey();
|
||||
_sessionId = sessionId;
|
||||
|
||||
textureRenderer.createTexture(_textureKey).then((id) async {
|
||||
if (id != -1) {
|
||||
final ptr = await textureRenderer.getTexturePtr(_textureKey);
|
||||
platformFFI.registerTexture(sessionId, ptr);
|
||||
platformFFI.registerTexture(sessionId, display, ptr);
|
||||
textureId.value = id;
|
||||
}
|
||||
});
|
||||
@@ -32,7 +37,7 @@ class RenderTexture {
|
||||
destroy(bool unregisterTexture) async {
|
||||
if (useTextureRender && _textureKey != -1 && _sessionId != null) {
|
||||
if (unregisterTexture) {
|
||||
platformFFI.registerTexture(_sessionId!, 0);
|
||||
platformFFI.registerTexture(_sessionId!, display, 0);
|
||||
}
|
||||
await textureRenderer.closeTexture(_textureKey);
|
||||
_textureKey = -1;
|
||||
|
||||
@@ -552,22 +552,22 @@ class InputModel {
|
||||
return v;
|
||||
}
|
||||
|
||||
Offset setNearestEdge(double x, double y, Display d) {
|
||||
double left = x - d.x;
|
||||
double right = d.x + d.width - 1 - x;
|
||||
double top = y - d.y;
|
||||
double bottom = d.y + d.height - 1 - y;
|
||||
Offset setNearestEdge(double x, double y, Rect rect) {
|
||||
double left = x - rect.left;
|
||||
double right = rect.right - 1 - x;
|
||||
double top = y - rect.top;
|
||||
double bottom = rect.bottom - 1 - y;
|
||||
if (left < right && left < top && left < bottom) {
|
||||
x = d.x;
|
||||
x = rect.left;
|
||||
}
|
||||
if (right < left && right < top && right < bottom) {
|
||||
x = d.x + d.width - 1;
|
||||
x = rect.right - 1;
|
||||
}
|
||||
if (top < left && top < right && top < bottom) {
|
||||
y = d.y;
|
||||
y = rect.top;
|
||||
}
|
||||
if (bottom < left && bottom < right && bottom < top) {
|
||||
y = d.y + d.height - 1;
|
||||
y = rect.bottom - 1;
|
||||
}
|
||||
return Offset(x, y);
|
||||
}
|
||||
@@ -711,9 +711,12 @@ class InputModel {
|
||||
final nearThr = 3;
|
||||
var nearRight = (canvasModel.size.width - x) < nearThr;
|
||||
var nearBottom = (canvasModel.size.height - y) < nearThr;
|
||||
final d = ffiModel.display;
|
||||
final imageWidth = d.width * canvasModel.scale;
|
||||
final imageHeight = d.height * canvasModel.scale;
|
||||
final rect = ffiModel.rect;
|
||||
if (rect == null) {
|
||||
return null;
|
||||
}
|
||||
final imageWidth = rect.width * canvasModel.scale;
|
||||
final imageHeight = rect.height * canvasModel.scale;
|
||||
if (canvasModel.scrollStyle == ScrollStyle.scrollbar) {
|
||||
x += imageWidth * canvasModel.scrollX;
|
||||
y += imageHeight * canvasModel.scrollY;
|
||||
@@ -741,11 +744,11 @@ class InputModel {
|
||||
y += step;
|
||||
}
|
||||
}
|
||||
x += d.x;
|
||||
y += d.y;
|
||||
x += rect.left;
|
||||
y += rect.top;
|
||||
|
||||
if (onExit) {
|
||||
final pos = setNearestEdge(x, y, d);
|
||||
final pos = setNearestEdge(x, y, rect);
|
||||
x = pos.dx;
|
||||
y = pos.dy;
|
||||
}
|
||||
@@ -761,10 +764,10 @@ class InputModel {
|
||||
return null;
|
||||
}
|
||||
|
||||
int minX = d.x.toInt();
|
||||
int maxX = (d.x + d.width).toInt() - 1;
|
||||
int minY = d.y.toInt();
|
||||
int maxY = (d.y + d.height).toInt() - 1;
|
||||
int minX = rect.left.toInt();
|
||||
int maxX = (rect.left + rect.width).toInt() - 1;
|
||||
int minY = rect.top.toInt();
|
||||
int maxY = (rect.top + rect.height).toInt() - 1;
|
||||
evtX = trySetNearestRange(evtX, minX, maxX, 5);
|
||||
evtY = trySetNearestRange(evtY, minY, maxY, 5);
|
||||
if (kind == kPointerEventKindMouse) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
@@ -18,6 +19,7 @@ import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/models/user_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
||||
import 'package:flutter_hbb/plugin/event.dart';
|
||||
import 'package:flutter_hbb/plugin/manager.dart';
|
||||
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
||||
@@ -86,7 +88,7 @@ class CachedPeerData {
|
||||
class FfiModel with ChangeNotifier {
|
||||
CachedPeerData cachedPeerData = CachedPeerData();
|
||||
PeerInfo _pi = PeerInfo();
|
||||
Display _display = Display();
|
||||
Rect? _rect;
|
||||
|
||||
var _inputBlocked = false;
|
||||
final _permissions = <String, bool>{};
|
||||
@@ -103,9 +105,15 @@ class FfiModel with ChangeNotifier {
|
||||
Timer? waitForImageTimer;
|
||||
RxBool waitForFirstImage = true.obs;
|
||||
|
||||
Map<String, bool> get permissions => _permissions;
|
||||
Rect? get rect => _rect;
|
||||
bool get isOriginalResolutionSet =>
|
||||
_pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolutionSet ?? false;
|
||||
bool get isVirtualDisplayResolution =>
|
||||
_pi.tryGetDisplayIfNotAllDisplay()?.isVirtualDisplayResolution ?? false;
|
||||
bool get isOriginalResolution =>
|
||||
_pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolution ?? false;
|
||||
|
||||
Display get display => _display;
|
||||
Map<String, bool> get permissions => _permissions;
|
||||
|
||||
bool? get secure => _secure;
|
||||
|
||||
@@ -130,6 +138,24 @@ class FfiModel with ChangeNotifier {
|
||||
sessionId = parent.target!.sessionId;
|
||||
}
|
||||
|
||||
Rect? displaysRect() {
|
||||
final displays = _pi.getCurDisplays();
|
||||
if (displays.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
double l = displays[0].x;
|
||||
double t = displays[0].y;
|
||||
double r = displays[0].x + displays[0].width;
|
||||
double b = displays[0].y + displays[0].height;
|
||||
for (var display in displays.sublist(1)) {
|
||||
l = min(l, display.x);
|
||||
t = min(t, display.y);
|
||||
r = max(r, display.x + display.width);
|
||||
b = max(b, display.y + display.height);
|
||||
}
|
||||
return Rect.fromLTRB(l, t, r, b);
|
||||
}
|
||||
|
||||
toggleTouchMode() {
|
||||
if (!isPeerAndroid) {
|
||||
_touchMode = !_touchMode;
|
||||
@@ -154,7 +180,6 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
clear() {
|
||||
_pi = PeerInfo();
|
||||
_display = Display();
|
||||
_secure = null;
|
||||
_direct = null;
|
||||
_inputBlocked = false;
|
||||
@@ -207,8 +232,10 @@ class FfiModel with ChangeNotifier {
|
||||
updateLastCursorId(element);
|
||||
await handleCursorData(element);
|
||||
}
|
||||
updateLastCursorId(data.lastCursorId);
|
||||
handleCursorId(data.lastCursorId);
|
||||
if (data.lastCursorId.isNotEmpty) {
|
||||
updateLastCursorId(data.lastCursorId);
|
||||
handleCursorId(data.lastCursorId);
|
||||
}
|
||||
}
|
||||
|
||||
// todo: why called by two position
|
||||
@@ -220,11 +247,12 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'peer_info') {
|
||||
handlePeerInfo(evt, peerId);
|
||||
} else if (name == 'sync_peer_info') {
|
||||
handleSyncPeerInfo(evt, sessionId);
|
||||
handleSyncPeerInfo(evt, sessionId, peerId);
|
||||
} else if (name == 'connection_ready') {
|
||||
setConnectionType(
|
||||
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
|
||||
} else if (name == 'switch_display') {
|
||||
// switch display is kept for backward compatibility
|
||||
handleSwitchDisplay(evt, sessionId, peerId);
|
||||
} else if (name == 'cursor_data') {
|
||||
updateLastCursorId(evt);
|
||||
@@ -279,7 +307,7 @@ class FfiModel with ChangeNotifier {
|
||||
await bind.sessionSwitchSides(sessionId: sessionId);
|
||||
closeConnection(id: peer_id);
|
||||
} else if (name == 'portable_service_running') {
|
||||
parent.target?.elevationModel.onPortableServiceRunning(evt);
|
||||
_handlePortableServiceRunning(peerId, evt);
|
||||
} else if (name == 'on_url_scheme_received') {
|
||||
// currently comes from "_url" ipc of mac and dbus of linux
|
||||
onUrlSchemeReceived(evt);
|
||||
@@ -354,20 +382,65 @@ class FfiModel with ChangeNotifier {
|
||||
platformFFI.setEventCallback(startEventListener(sessionId, peerId));
|
||||
}
|
||||
|
||||
_updateCurDisplay(SessionID sessionId, Display newDisplay) {
|
||||
if (newDisplay != _display) {
|
||||
if (newDisplay.x != _display.x || newDisplay.y != _display.y) {
|
||||
parent.target?.cursorModel
|
||||
.updateDisplayOrigin(newDisplay.x, newDisplay.y);
|
||||
_handlePortableServiceRunning(String peerId, Map<String, dynamic> evt) {
|
||||
final running = evt['running'] == 'true';
|
||||
parent.target?.elevationModel.onPortableServiceRunning(running);
|
||||
if (running) {
|
||||
if (pi.primaryDisplay != kInvalidDisplayIndex) {
|
||||
if (pi.currentDisplay != pi.primaryDisplay) {
|
||||
// Notify to switch display
|
||||
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
|
||||
'elevated_switch_display_msg', '', parent.target!.dialogManager);
|
||||
bind.sessionSwitchDisplay(
|
||||
sessionId: sessionId,
|
||||
value: Int32List.fromList([pi.primaryDisplay]));
|
||||
}
|
||||
}
|
||||
_display = newDisplay;
|
||||
}
|
||||
}
|
||||
|
||||
handleAliasChanged(Map<String, dynamic> evt) {
|
||||
if (!isDesktop) return;
|
||||
final String peerId = evt['id'];
|
||||
final String alias = evt['alias'];
|
||||
String label = getDesktopTabLabel(peerId, alias);
|
||||
final rxTabLabel = PeerStringOption.find(evt['id'], 'tabLabel');
|
||||
if (rxTabLabel.value != label) {
|
||||
rxTabLabel.value = label;
|
||||
}
|
||||
}
|
||||
|
||||
updateCurDisplay(SessionID sessionId) {
|
||||
final newRect = displaysRect();
|
||||
if (newRect == null) {
|
||||
return;
|
||||
}
|
||||
if (newRect != _rect) {
|
||||
if (newRect.left != _rect?.left || newRect.top != _rect?.top) {
|
||||
parent.target?.cursorModel
|
||||
.updateDisplayOrigin(newRect.left, newRect.top);
|
||||
}
|
||||
_rect = newRect;
|
||||
parent.target?.canvasModel.updateViewStyle();
|
||||
_updateSessionWidthHeight(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
handleSwitchDisplay(
|
||||
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
|
||||
_pi.currentDisplay = int.parse(evt['display']);
|
||||
final curDisplay = int.parse(evt['display']);
|
||||
|
||||
// The message should be handled by the another UI session.
|
||||
if (isChooseDisplayToOpenInNewWindow(_pi, sessionId)) {
|
||||
if (curDisplay != _pi.currentDisplay) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_pi.currentDisplay != kAllDisplayValue) {
|
||||
_pi.currentDisplay = curDisplay;
|
||||
}
|
||||
|
||||
var newDisplay = Display();
|
||||
newDisplay.x = double.tryParse(evt['x']) ?? newDisplay.x;
|
||||
newDisplay.y = double.tryParse(evt['y']) ?? newDisplay.y;
|
||||
@@ -378,11 +451,11 @@ class FfiModel with ChangeNotifier {
|
||||
int.tryParse(evt['original_width']) ?? kInvalidResolutionValue;
|
||||
newDisplay.originalHeight =
|
||||
int.tryParse(evt['original_height']) ?? kInvalidResolutionValue;
|
||||
_pi.displays[curDisplay] = newDisplay;
|
||||
|
||||
_updateCurDisplay(sessionId, newDisplay);
|
||||
|
||||
updateCurDisplay(sessionId);
|
||||
try {
|
||||
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
|
||||
CurrentDisplayState.find(peerId).value = curDisplay;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
@@ -522,13 +595,30 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
_updateSessionWidthHeight(SessionID sessionId) {
|
||||
parent.target?.canvasModel.updateViewStyle();
|
||||
if (display.width <= 0 || display.height <= 0) {
|
||||
if (_rect == null) return;
|
||||
if (_rect!.width <= 0 || _rect!.height <= 0) {
|
||||
debugPrintStack(
|
||||
label: 'invalid display size (${display.width},${display.height})');
|
||||
label: 'invalid display size (${_rect!.width},${_rect!.height})');
|
||||
} else {
|
||||
bind.sessionSetSize(
|
||||
sessionId: sessionId, width: display.width, height: display.height);
|
||||
final displays = _pi.getCurDisplays();
|
||||
if (displays.length == 1) {
|
||||
bind.sessionSetSize(
|
||||
sessionId: sessionId,
|
||||
display:
|
||||
pi.currentDisplay == kAllDisplayValue ? 0 : pi.currentDisplay,
|
||||
width: _rect!.width.toInt(),
|
||||
height: _rect!.height.toInt(),
|
||||
);
|
||||
} else {
|
||||
for (int i = 0; i < displays.length; ++i) {
|
||||
bind.sessionSetSize(
|
||||
sessionId: sessionId,
|
||||
display: i,
|
||||
width: displays[i].width.toInt(),
|
||||
height: displays[i].height.toInt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,11 +631,20 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
parent.target?.dialogManager.dismissAll();
|
||||
_pi.version = evt['version'];
|
||||
_pi.isSupportMultiUiSession =
|
||||
bind.isSupportMultiUiSession(version: _pi.version);
|
||||
_pi.username = evt['username'];
|
||||
_pi.hostname = evt['hostname'];
|
||||
_pi.platform = evt['platform'];
|
||||
_pi.sasEnabled = evt['sas_enabled'] == 'true';
|
||||
_pi.currentDisplay = int.parse(evt['current_display']);
|
||||
final currentDisplay = int.parse(evt['current_display']);
|
||||
if (_pi.primaryDisplay == kInvalidDisplayIndex) {
|
||||
_pi.primaryDisplay = currentDisplay;
|
||||
}
|
||||
|
||||
if (bind.peerGetDefaultSessionsCount(id: peerId) <= 1) {
|
||||
_pi.currentDisplay = currentDisplay;
|
||||
}
|
||||
|
||||
try {
|
||||
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
|
||||
@@ -569,10 +668,10 @@ class FfiModel with ChangeNotifier {
|
||||
for (int i = 0; i < displays.length; ++i) {
|
||||
_pi.displays.add(evtToDisplay(displays[i]));
|
||||
}
|
||||
stateGlobal.displaysCount.value = _pi.displays.length;
|
||||
_pi.displaysCount.value = _pi.displays.length;
|
||||
if (_pi.currentDisplay < _pi.displays.length) {
|
||||
_display = _pi.displays[_pi.currentDisplay];
|
||||
_updateSessionWidthHeight(sessionId);
|
||||
// now replaced to _updateCurDisplay
|
||||
updateCurDisplay(sessionId);
|
||||
}
|
||||
if (displays.isNotEmpty) {
|
||||
_reconnects = 1;
|
||||
@@ -590,12 +689,12 @@ class FfiModel with ChangeNotifier {
|
||||
sessionId: sessionId, arg: 'view-only'));
|
||||
}
|
||||
if (connType == ConnType.defaultConn) {
|
||||
final platform_additions = evt['platform_additions'];
|
||||
if (platform_additions != null && platform_additions != '') {
|
||||
final platformDdditions = evt['platform_additions'];
|
||||
if (platformDdditions != null && platformDdditions != '') {
|
||||
try {
|
||||
_pi.platform_additions = json.decode(platform_additions);
|
||||
_pi.platformDdditions = json.decode(platformDdditions);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to decode platform_additions $e');
|
||||
debugPrint('Failed to decode platformDdditions $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -670,7 +769,8 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Handle the peer info synchronization event based on [evt].
|
||||
handleSyncPeerInfo(Map<String, dynamic> evt, SessionID sessionId) async {
|
||||
handleSyncPeerInfo(
|
||||
Map<String, dynamic> evt, SessionID sessionId, String peerId) async {
|
||||
if (evt['displays'] != null) {
|
||||
cachedPeerData.peerInfo['displays'] = evt['displays'];
|
||||
List<dynamic> displays = json.decode(evt['displays']);
|
||||
@@ -679,14 +779,54 @@ class FfiModel with ChangeNotifier {
|
||||
newDisplays.add(evtToDisplay(displays[i]));
|
||||
}
|
||||
_pi.displays = newDisplays;
|
||||
stateGlobal.displaysCount.value = _pi.displays.length;
|
||||
if (_pi.currentDisplay >= 0 && _pi.currentDisplay < _pi.displays.length) {
|
||||
_updateCurDisplay(sessionId, _pi.displays[_pi.currentDisplay]);
|
||||
_pi.displaysCount.value = _pi.displays.length;
|
||||
if (_pi.currentDisplay == kAllDisplayValue) {
|
||||
updateCurDisplay(sessionId);
|
||||
// to-do: What if the displays are changed?
|
||||
} else {
|
||||
if (_pi.currentDisplay >= 0 &&
|
||||
_pi.currentDisplay < _pi.displays.length) {
|
||||
updateCurDisplay(sessionId);
|
||||
} else {
|
||||
if (_pi.displays.isNotEmpty) {
|
||||
// Notify to switch display
|
||||
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
|
||||
'display_is_plugged_out_msg', '', parent.target!.dialogManager);
|
||||
final newDisplay = pi.primaryDisplay == kInvalidDisplayIndex
|
||||
? 0
|
||||
: pi.primaryDisplay;
|
||||
final displays = newDisplay;
|
||||
bind.sessionSwitchDisplay(
|
||||
sessionId: sessionId, value: Int32List.fromList([displays]));
|
||||
|
||||
if (_pi.isSupportMultiUiSession) {
|
||||
// If the peer supports multi-ui-session, no switch display message will be send back.
|
||||
// We need to update the display manually.
|
||||
switchToNewDisplay(newDisplay, sessionId, peerId);
|
||||
}
|
||||
} else {
|
||||
msgBox(sessionId, 'nocancel-error', 'Prompt', 'No Displays', '',
|
||||
parent.target!.dialogManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Directly switch to the new display without waiting for the response.
|
||||
switchToNewDisplay(int display, SessionID sessionId, String peerId) {
|
||||
// no need to wait for the response
|
||||
pi.currentDisplay = display;
|
||||
updateCurDisplay(sessionId);
|
||||
try {
|
||||
CurrentDisplayState.find(peerId).value = display;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
parent.target?.recordingModel.onSwitchDisplay();
|
||||
}
|
||||
|
||||
updateBlockInputState(Map<String, dynamic> evt, String peerId) {
|
||||
_inputBlocked = evt['input_state'] == 'on';
|
||||
notifyListeners();
|
||||
@@ -709,7 +849,7 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
void setViewOnly(String id, bool value) {
|
||||
if (version_cmp(_pi.version, '1.2.0') < 0) return;
|
||||
if (versionCmp(_pi.version, '1.2.0') < 0) return;
|
||||
// tmp fix for https://github.com/rustdesk/rustdesk/pull/3706#issuecomment-1481242389
|
||||
// because below rx not used in mobile version, so not initialized, below code will cause crash
|
||||
// current our flutter code quality is fucking shit now. !!!!!!!!!!!!!!!!
|
||||
@@ -749,16 +889,16 @@ class ImageModel with ChangeNotifier {
|
||||
|
||||
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
|
||||
|
||||
onRgba(Uint8List rgba) {
|
||||
onRgba(int display, Uint8List rgba) {
|
||||
final pid = parent.target?.id;
|
||||
img.decodeImageFromPixels(
|
||||
rgba,
|
||||
parent.target?.ffiModel.display.width ?? 0,
|
||||
parent.target?.ffiModel.display.height ?? 0,
|
||||
parent.target?.ffiModel.rect?.width.toInt() ?? 0,
|
||||
parent.target?.ffiModel.rect?.height.toInt() ?? 0,
|
||||
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
|
||||
onPixelsCopied: () {
|
||||
// Unlock the rgba memory from rust codes.
|
||||
platformFFI.nextRgba(sessionId);
|
||||
platformFFI.nextRgba(sessionId, display);
|
||||
}).then((image) {
|
||||
if (parent.target?.id != pid) return;
|
||||
try {
|
||||
@@ -1017,20 +1157,20 @@ class CanvasModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
bool get cursorEmbedded =>
|
||||
parent.target?.ffiModel.display.cursorEmbedded ?? false;
|
||||
parent.target?.ffiModel._pi.cursorEmbedded ?? false;
|
||||
|
||||
int getDisplayWidth() {
|
||||
final defaultWidth = (isDesktop || isWebDesktop)
|
||||
? kDesktopDefaultDisplayWidth
|
||||
: kMobileDefaultDisplayWidth;
|
||||
return parent.target?.ffiModel.display.width ?? defaultWidth;
|
||||
return parent.target?.ffiModel.rect?.width.toInt() ?? defaultWidth;
|
||||
}
|
||||
|
||||
int getDisplayHeight() {
|
||||
final defaultHeight = (isDesktop || isWebDesktop)
|
||||
? kDesktopDefaultDisplayHeight
|
||||
: kMobileDefaultDisplayHeight;
|
||||
return parent.target?.ffiModel.display.height ?? defaultHeight;
|
||||
return parent.target?.ffiModel.rect?.height.toInt() ?? defaultHeight;
|
||||
}
|
||||
|
||||
static double get windowBorderWidth => stateGlobal.windowBorderWidth.value;
|
||||
@@ -1619,7 +1759,27 @@ 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['fps'] as String).isNotEmpty) {
|
||||
final fps = jsonDecode(evt['fps']) as Map<String, dynamic>;
|
||||
final pi = parent.target?.ffiModel.pi;
|
||||
if (pi != null) {
|
||||
final currentDisplay = pi.currentDisplay;
|
||||
if (currentDisplay != kAllDisplayValue) {
|
||||
final fps2 = fps[currentDisplay.toString()];
|
||||
if (fps2 != null) {
|
||||
_data.fps = fps2.toString();
|
||||
}
|
||||
} else if (fps.isNotEmpty) {
|
||||
final fpsList = [];
|
||||
for (var i = 0; i < pi.displays.length; i++) {
|
||||
fpsList.add((fps[i.toString()] ?? 0).toString());
|
||||
}
|
||||
_data.fps = fpsList.join(' ');
|
||||
}
|
||||
} else {
|
||||
_data.fps = null;
|
||||
}
|
||||
}
|
||||
if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay'];
|
||||
if ((evt['target_bitrate'] as String).isNotEmpty) {
|
||||
_data.targetBitrate = evt['target_bitrate'];
|
||||
@@ -1646,8 +1806,15 @@ class RecordingModel with ChangeNotifier {
|
||||
int? width = parent.target?.canvasModel.getDisplayWidth();
|
||||
int? height = parent.target?.canvasModel.getDisplayHeight();
|
||||
if (sessionId == null || width == null || height == null) return;
|
||||
bind.sessionRecordScreen(
|
||||
sessionId: sessionId, start: true, width: width, height: height);
|
||||
final currentDisplay = parent.target?.ffiModel.pi.currentDisplay;
|
||||
if (currentDisplay != kAllDisplayValue) {
|
||||
bind.sessionRecordScreen(
|
||||
sessionId: sessionId,
|
||||
start: true,
|
||||
display: currentDisplay!,
|
||||
width: width,
|
||||
height: height);
|
||||
}
|
||||
}
|
||||
|
||||
toggle() async {
|
||||
@@ -1658,10 +1825,20 @@ class RecordingModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
await bind.sessionRecordStatus(sessionId: sessionId, status: _start);
|
||||
if (_start) {
|
||||
bind.sessionRefresh(sessionId: sessionId);
|
||||
final pi = parent.target?.ffiModel.pi;
|
||||
if (pi != null) {
|
||||
sessionRefreshVideo(sessionId, pi);
|
||||
}
|
||||
} else {
|
||||
bind.sessionRecordScreen(
|
||||
sessionId: sessionId, start: false, width: 0, height: 0);
|
||||
final currentDisplay = parent.target?.ffiModel.pi.currentDisplay;
|
||||
if (currentDisplay != kAllDisplayValue) {
|
||||
bind.sessionRecordScreen(
|
||||
sessionId: sessionId,
|
||||
start: false,
|
||||
display: currentDisplay!,
|
||||
width: 0,
|
||||
height: 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1670,8 +1847,15 @@ class RecordingModel with ChangeNotifier {
|
||||
final sessionId = parent.target?.sessionId;
|
||||
if (sessionId == null) return;
|
||||
_start = false;
|
||||
bind.sessionRecordScreen(
|
||||
sessionId: sessionId, start: false, width: 0, height: 0);
|
||||
final currentDisplay = parent.target?.ffiModel.pi.currentDisplay;
|
||||
if (currentDisplay != kAllDisplayValue) {
|
||||
bind.sessionRecordScreen(
|
||||
sessionId: sessionId,
|
||||
start: false,
|
||||
display: currentDisplay!,
|
||||
width: 0,
|
||||
height: 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1686,9 +1870,7 @@ class ElevationModel with ChangeNotifier {
|
||||
_running = false;
|
||||
}
|
||||
|
||||
onPortableServiceRunning(Map<String, dynamic> evt) {
|
||||
_running = evt['running'] == 'true';
|
||||
}
|
||||
onPortableServiceRunning(bool running) => _running = running;
|
||||
}
|
||||
|
||||
enum ConnType { defaultConn, fileTransfer, portForward, rdp }
|
||||
@@ -1751,14 +1933,18 @@ class FFI {
|
||||
}
|
||||
|
||||
/// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward].
|
||||
void start(String id,
|
||||
{bool isFileTransfer = false,
|
||||
bool isPortForward = false,
|
||||
bool isRdp = false,
|
||||
String? switchUuid,
|
||||
String? password,
|
||||
bool? forceRelay,
|
||||
int? tabWindowId}) {
|
||||
void start(
|
||||
String id, {
|
||||
bool isFileTransfer = false,
|
||||
bool isPortForward = false,
|
||||
bool isRdp = false,
|
||||
String? switchUuid,
|
||||
String? password,
|
||||
bool? forceRelay,
|
||||
int? tabWindowId,
|
||||
int? display,
|
||||
List<int>? displays,
|
||||
}) {
|
||||
closed = false;
|
||||
auditNote = '';
|
||||
if (isMobile) mobileReset();
|
||||
@@ -1788,10 +1974,32 @@ class FFI {
|
||||
forceRelay: forceRelay ?? false,
|
||||
password: password ?? '',
|
||||
);
|
||||
} else if (display != null) {
|
||||
if (displays == null) {
|
||||
debugPrint(
|
||||
'Unreachable, failed to add existed session to $id, the displays is null while display is $display');
|
||||
return;
|
||||
}
|
||||
final addRes = bind.sessionAddExistedSync(id: id, sessionId: sessionId);
|
||||
if (addRes != '') {
|
||||
debugPrint(
|
||||
'Unreachable, failed to add existed session to $id, $addRes');
|
||||
return;
|
||||
}
|
||||
bind.sessionTryAddDisplay(
|
||||
sessionId: sessionId, displays: Int32List.fromList(displays));
|
||||
ffiModel.pi.currentDisplay = display;
|
||||
}
|
||||
final stream = bind.sessionStart(sessionId: sessionId, id: id);
|
||||
final cb = ffiModel.startEventListener(sessionId, id);
|
||||
final useTextureRender = bind.mainUseTextureRender();
|
||||
|
||||
// Force refresh displays.
|
||||
// The controlled side may not refresh the image when the (peer,display) is already subscribed.
|
||||
if (displays != null) {
|
||||
for (final display in displays) {
|
||||
bind.sessionRefresh(sessionId: sessionId, display: display);
|
||||
}
|
||||
}
|
||||
|
||||
final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
|
||||
// Preserved for the rgba data.
|
||||
@@ -1801,8 +2009,9 @@ class FFI {
|
||||
// Session is read to be moved to a new window.
|
||||
// Get the cached data and handle the cached data.
|
||||
Future.delayed(Duration.zero, () async {
|
||||
final args = jsonEncode({'id': id, 'close': display == null});
|
||||
final cachedData = await DesktopMultiWindow.invokeMethod(
|
||||
tabWindowId, kWindowEventGetCachedSessionData, id);
|
||||
tabWindowId, kWindowEventGetCachedSessionData, args);
|
||||
if (cachedData == null) {
|
||||
// unreachable
|
||||
debugPrint('Unreachable, the cached data is empty.');
|
||||
@@ -1814,7 +2023,7 @@ class FFI {
|
||||
return;
|
||||
}
|
||||
await ffiModel.handleCachedPeerData(data, id);
|
||||
await bind.sessionRefresh(sessionId: sessionId);
|
||||
await sessionRefreshVideo(sessionId, ffiModel.pi);
|
||||
});
|
||||
isToNewWindowNotified.value = true;
|
||||
}
|
||||
@@ -1836,18 +2045,19 @@ class FFI {
|
||||
await cb(event);
|
||||
}
|
||||
} else if (message is EventToUI_Rgba) {
|
||||
final display = message.field0;
|
||||
if (useTextureRender) {
|
||||
onEvent2UIRgba();
|
||||
} else {
|
||||
// Fetch the image buffer from rust codes.
|
||||
final sz = platformFFI.getRgbaSize(sessionId);
|
||||
final sz = platformFFI.getRgbaSize(sessionId, display);
|
||||
if (sz == 0) {
|
||||
return;
|
||||
}
|
||||
final rgba = platformFFI.getRgba(sessionId, sz);
|
||||
final rgba = platformFFI.getRgba(sessionId, display, sz);
|
||||
if (rgba != null) {
|
||||
onEvent2UIRgba();
|
||||
imageModel.onRgba(rgba);
|
||||
imageModel.onRgba(display, rgba);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1979,22 +2189,72 @@ class Features {
|
||||
bool privacyMode = false;
|
||||
}
|
||||
|
||||
const kInvalidDisplayIndex = -1;
|
||||
|
||||
class PeerInfo with ChangeNotifier {
|
||||
String version = '';
|
||||
String username = '';
|
||||
String hostname = '';
|
||||
String platform = '';
|
||||
bool sasEnabled = false;
|
||||
bool isSupportMultiUiSession = false;
|
||||
int currentDisplay = 0;
|
||||
int primaryDisplay = kInvalidDisplayIndex;
|
||||
List<Display> displays = [];
|
||||
Features features = Features();
|
||||
List<Resolution> resolutions = [];
|
||||
Map<String, dynamic> platform_additions = {};
|
||||
Map<String, dynamic> platformDdditions = {};
|
||||
|
||||
RxInt displaysCount = 0.obs;
|
||||
RxBool isSet = false.obs;
|
||||
|
||||
bool get is_wayland => platform_additions['is_wayland'] == true;
|
||||
bool get is_headless => platform_additions['headless'] == true;
|
||||
bool get isWayland => platformDdditions['is_wayland'] == true;
|
||||
bool get isHeadless => platformDdditions['headless'] == true;
|
||||
|
||||
bool get isSupportMultiDisplay => isDesktop && isSupportMultiUiSession;
|
||||
|
||||
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;
|
||||
|
||||
Display? tryGetDisplay() {
|
||||
if (displays.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (currentDisplay == kAllDisplayValue) {
|
||||
return displays[0];
|
||||
} else {
|
||||
if (currentDisplay > 0 && currentDisplay < displays.length) {
|
||||
return displays[currentDisplay];
|
||||
} else {
|
||||
return displays[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Display? tryGetDisplayIfNotAllDisplay() {
|
||||
if (displays.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (currentDisplay == kAllDisplayValue) {
|
||||
return null;
|
||||
}
|
||||
if (currentDisplay > 0 && currentDisplay < displays.length) {
|
||||
return displays[currentDisplay];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<Display> getCurDisplays() {
|
||||
if (currentDisplay == kAllDisplayValue) {
|
||||
return displays;
|
||||
} else {
|
||||
if (currentDisplay >= 0 && currentDisplay < displays.length) {
|
||||
return [displays[currentDisplay]];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const canvasKey = 'canvas';
|
||||
@@ -2038,8 +2298,8 @@ Future<void> initializeCursorAndCanvas(FFI ffi) async {
|
||||
currentDisplay = p['currentDisplay'];
|
||||
}
|
||||
if (p == null || currentDisplay != ffi.ffiModel.pi.currentDisplay) {
|
||||
ffi.cursorModel
|
||||
.updateDisplayOrigin(ffi.ffiModel.display.x, ffi.ffiModel.display.y);
|
||||
ffi.cursorModel.updateDisplayOrigin(
|
||||
ffi.ffiModel.rect?.left ?? 0, ffi.ffiModel.rect?.top ?? 0);
|
||||
return;
|
||||
}
|
||||
double xCursor = p['xCursor'];
|
||||
@@ -2047,8 +2307,8 @@ Future<void> initializeCursorAndCanvas(FFI ffi) async {
|
||||
double xCanvas = p['xCanvas'];
|
||||
double yCanvas = p['yCanvas'];
|
||||
double scale = p['scale'];
|
||||
ffi.cursorModel.updateDisplayOriginWithCursor(
|
||||
ffi.ffiModel.display.x, ffi.ffiModel.display.y, xCursor, yCursor);
|
||||
ffi.cursorModel.updateDisplayOriginWithCursor(ffi.ffiModel.rect?.left ?? 0,
|
||||
ffi.ffiModel.rect?.top ?? 0, xCursor, yCursor);
|
||||
ffi.canvasModel.update(xCanvas, yCanvas, scale);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ class RgbaFrame extends Struct {
|
||||
external Pointer<Uint8> data;
|
||||
}
|
||||
|
||||
typedef F3 = Pointer<Uint8> Function(Pointer<Utf8>);
|
||||
typedef F3 = Pointer<Uint8> Function(Pointer<Utf8>, int);
|
||||
typedef F3Dart = Pointer<Uint8> Function(Pointer<Utf8>, Int32);
|
||||
typedef HandleEvent = Future<void> Function(Map<String, dynamic> evt);
|
||||
|
||||
/// FFI wrapper around the native Rust core.
|
||||
@@ -80,12 +81,12 @@ class PlatformFFI {
|
||||
String translate(String name, String locale) =>
|
||||
_ffiBind.translate(name: name, locale: locale);
|
||||
|
||||
Uint8List? getRgba(SessionID sessionId, int bufSize) {
|
||||
Uint8List? getRgba(SessionID sessionId, int display, int bufSize) {
|
||||
if (_session_get_rgba == null) return null;
|
||||
final sessionIdStr = sessionId.toString();
|
||||
var a = sessionIdStr.toNativeUtf8();
|
||||
try {
|
||||
final buffer = _session_get_rgba!(a);
|
||||
final buffer = _session_get_rgba!(a, display);
|
||||
if (buffer == nullptr) {
|
||||
return null;
|
||||
}
|
||||
@@ -96,12 +97,11 @@ class PlatformFFI {
|
||||
}
|
||||
}
|
||||
|
||||
int getRgbaSize(SessionID sessionId) =>
|
||||
_ffiBind.sessionGetRgbaSize(sessionId: sessionId);
|
||||
void nextRgba(SessionID sessionId) =>
|
||||
_ffiBind.sessionNextRgba(sessionId: sessionId);
|
||||
void registerTexture(SessionID sessionId, int ptr) =>
|
||||
_ffiBind.sessionRegisterTexture(sessionId: sessionId, ptr: ptr);
|
||||
int getRgbaSize(SessionID sessionId, int display) =>
|
||||
_ffiBind.sessionGetRgbaSize(sessionId: sessionId, display: display);
|
||||
void nextRgba(SessionID sessionId, int display) => _ffiBind.sessionNextRgba(sessionId: sessionId, display: display);
|
||||
void registerTexture(SessionID sessionId, int display, int ptr) =>
|
||||
_ffiBind.sessionRegisterTexture(sessionId: sessionId, display: display, ptr: ptr);
|
||||
|
||||
/// Init the FFI class, loads the native Rust core library.
|
||||
Future<void> init(String appType) async {
|
||||
@@ -117,7 +117,7 @@ class PlatformFFI {
|
||||
: DynamicLibrary.process();
|
||||
debugPrint('initializing FFI $_appType');
|
||||
try {
|
||||
_session_get_rgba = dylib.lookupFunction<F3, F3>("session_get_rgba");
|
||||
_session_get_rgba = dylib.lookupFunction<F3Dart, F3>("session_get_rgba");
|
||||
try {
|
||||
// SYSTEM user failed
|
||||
_dir = (await getApplicationDocumentsDirectory()).path;
|
||||
|
||||
@@ -18,7 +18,6 @@ class StateGlobal {
|
||||
final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize);
|
||||
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
|
||||
final RxBool showRemoteToolBar = false.obs;
|
||||
final RxInt displaysCount = 0.obs;
|
||||
final svcStatus = SvcStatus.notReady.obs;
|
||||
// Only used for macOS
|
||||
bool closeOnFullscreen = false;
|
||||
|
||||
@@ -67,6 +67,44 @@ class RustDeskMultiWindowManager {
|
||||
);
|
||||
}
|
||||
|
||||
// This function must be called in the main window thread.
|
||||
// Because the _remoteDesktopWindows is managed in that thread.
|
||||
openMonitorSession(
|
||||
int windowId, String peerId, int display, int displayCount) async {
|
||||
if (_remoteDesktopWindows.length > 1) {
|
||||
for (final windowId in _remoteDesktopWindows) {
|
||||
if (await DesktopMultiWindow.invokeMethod(
|
||||
windowId,
|
||||
kWindowEventActiveDisplaySession,
|
||||
jsonEncode({
|
||||
'id': peerId,
|
||||
'display': display,
|
||||
}))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final displays = display == kAllDisplayValue
|
||||
? List.generate(displayCount, (index) => index)
|
||||
: [display];
|
||||
var params = {
|
||||
'type': WindowType.RemoteDesktop.index,
|
||||
'id': peerId,
|
||||
'tab_window_id': windowId,
|
||||
'display': display,
|
||||
'displays': displays,
|
||||
};
|
||||
await _newSession(
|
||||
false,
|
||||
WindowType.RemoteDesktop,
|
||||
kWindowEventNewRemoteDesktop,
|
||||
peerId,
|
||||
_remoteDesktopWindows,
|
||||
jsonEncode(params),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> newSessionWindow(
|
||||
WindowType type, String remoteId, String msg, List<int> windows) async {
|
||||
final windowController = await DesktopMultiWindow.createWindow(msg);
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||
version: 1.2.3+39
|
||||
version: 1.2.4+39
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.0"
|
||||
|
||||
Reference in New Issue
Block a user