Merge branch 'master' of https://github.com/rustdesk/rustdesk into opt_chat_overlay_and_fix_pageview_2

This commit is contained in:
csf
2023-02-08 22:29:51 +09:00
161 changed files with 3439 additions and 1118 deletions

View File

@@ -3,14 +3,11 @@ import 'dart:convert';
import 'dart:ffi' hide Size;
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:back_button_interceptor/back_button_interceptor.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:win32/win32.dart' as win32;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -19,14 +16,17 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:uni_links/uni_links.dart';
import 'package:uni_links_desktop/uni_links_desktop.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:window_size/window_size.dart' as window_size;
import 'package:url_launcher/url_launcher.dart';
import 'package:win32/win32.dart' as win32;
import 'package:window_manager/window_manager.dart';
import 'package:window_size/window_size.dart' as window_size;
import '../consts.dart';
import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart';
@@ -34,8 +34,6 @@ import 'models/input_model.dart';
import 'models/model.dart';
import 'models/platform_model.dart';
import '../consts.dart';
final globalKey = GlobalKey<NavigatorState>();
final navigationBarKey = GlobalKey();
@@ -702,7 +700,6 @@ void msgBox(String id, String type, String title, String text, String link,
buttons.insert(
0, dialogButton('Cancel', onPressed: cancel, isOutline: true));
}
// TODO: test this button
if (type.contains("hasclose")) {
buttons.insert(
0,
@@ -716,8 +713,7 @@ void msgBox(String id, String type, String title, String text, String link,
dialogManager.show(
(setState, close) => CustomAlertDialog(
title: null,
content: SelectionArea(
child: msgboxContent(type, title, text).paddingOnly(bottom: 10)),
content: SelectionArea(child: msgboxContent(type, title, text)),
actions: buttons,
onSubmit: hasOk ? submit : null,
onCancel: hasCancel == true ? cancel : null,
@@ -782,7 +778,7 @@ Widget msgboxContent(String type, String title, String text) {
),
),
],
);
).marginOnly(bottom: 12);
}
void msgBoxCommon(OverlayDialogManager dialogManager, String title,
@@ -1280,10 +1276,12 @@ Future<bool> restoreWindowPosition(WindowType type, {int? windowId}) async {
/// [Availability]
/// initUniLinks should only be used on macos/windows.
/// we use dbus for linux currently.
Future<void> initUniLinks() async {
if (!Platform.isWindows && !Platform.isMacOS) {
return;
Future<bool> initUniLinks() async {
if (Platform.isLinux) {
return false;
}
// Register uni links for Windows. The required info of url scheme is already
// declared in `Info.plist` for macOS.
if (Platform.isWindows) {
registerProtocol('rustdesk');
}
@@ -1291,22 +1289,33 @@ Future<void> initUniLinks() async {
try {
final initialLink = await getInitialLink();
if (initialLink == null) {
return;
return false;
}
parseRustdeskUri(initialLink);
return parseRustdeskUri(initialLink);
} catch (err) {
debugPrintStack(label: "$err");
return false;
}
}
StreamSubscription? listenUniLinks() {
if (!(Platform.isWindows || Platform.isMacOS)) {
/// Listen for uni links.
///
/// * handleByFlutter: Should uni links be handled by Flutter.
///
/// Returns a [StreamSubscription] which can listen the uni links.
StreamSubscription? listenUniLinks({handleByFlutter = true}) {
if (Platform.isLinux) {
return null;
}
final sub = uriLinkStream.listen((Uri? uri) {
debugPrint("A uri was received: $uri.");
if (uri != null) {
callUniLinksUriHandler(uri);
if (handleByFlutter) {
callUniLinksUriHandler(uri);
} else {
bind.sendUrlScheme(url: uri.toString());
}
} else {
print("uni listen error: uri is empty.");
}
@@ -1316,11 +1325,19 @@ StreamSubscription? listenUniLinks() {
return sub;
}
/// Returns true if we successfully handle the startup arguments.
/// Handle command line arguments
///
/// * Returns true if we successfully handle the startup arguments.
bool checkArguments() {
if (kBootArgs.isNotEmpty) {
final ret = parseRustdeskUri(kBootArgs.first);
if (ret) {
return true;
}
}
// bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05]
// check connect args
final connectIndex = kBootArgs.indexOf("--connect");
var connectIndex = kBootArgs.indexOf("--connect");
if (connectIndex == -1) {
return false;
}
@@ -1355,7 +1372,7 @@ bool checkArguments() {
bool parseRustdeskUri(String uriPath) {
final uri = Uri.tryParse(uriPath);
if (uri == null) {
print("uri is not valid: $uriPath");
debugPrint("uri is not valid: $uriPath");
return false;
}
return callUniLinksUriHandler(uri);
@@ -1374,7 +1391,7 @@ bool callUniLinksUriHandler(Uri uri) {
Future.delayed(Duration.zero, () {
rustDeskWinManager.newRemoteDesktop(peerId, switch_uuid: switch_uuid);
});
return false;
return true;
}
return false;
}
@@ -1514,8 +1531,12 @@ Future<void> onActiveWindowChanged() async {
} catch (err) {
debugPrintStack(label: "$err");
} finally {
debugPrint("Start closing RustDesk...");
await windowManager.setPreventClose(false);
await windowManager.close();
if (Platform.isMacOS) {
RdPlatformChannel.instance.terminate();
}
}
}
}
@@ -1708,3 +1729,30 @@ Future<void> updateSystemWindowTheme() async {
}
}
}
/// macOS only
///
/// Note: not found a general solution for rust based AVFoundation bingding.
/// [AVFoundation] crate has compile error.
const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos");
enum PermissionAuthorizeType {
undetermined,
authorized,
denied, // and restricted
}
Future<PermissionAuthorizeType> osxCanRecordAudio() async {
int res = await kMacOSPermChannel.invokeMethod("canRecordAudio");
print(res);
if (res > 0) {
return PermissionAuthorizeType.authorized;
} else if (res == 0) {
return PermissionAuthorizeType.undetermined;
} else {
return PermissionAuthorizeType.denied;
}
}
Future<bool> osxRequestAudio() async {
return await kMacOSPermChannel.invokeMethod("requestRecordAudio");
}

View File

@@ -1,6 +1,7 @@
import 'dart:ui' as ui;
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/address_book.dart';
import 'package:flutter_hbb/common/widgets/my_group.dart';
@@ -11,106 +12,15 @@ import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu;
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart';
import '../../common.dart';
import '../../models/platform_model.dart';
const int groupTabIndex = 4;
const String defaultGroupTabname = 'Group';
class StatePeerTab {
final RxInt currentTab = 0.obs;
final RxInt tabHiddenFlag = 0.obs;
final RxList<String> tabNames = [
'Recent Sessions',
'Favorites',
'Discovered',
'Address Book',
defaultGroupTabname,
].obs;
StatePeerTab._() {
tabHiddenFlag.value = (int.tryParse(
bind.getLocalFlutterConfig(k: 'hidden-peer-card'),
radix: 2) ??
0);
var tabs = _notHiddenTabs();
currentTab.value =
int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0;
if (!tabs.contains(currentTab.value)) {
currentTab.value = 0;
}
}
static final StatePeerTab instance = StatePeerTab._();
check() {
var tabs = _notHiddenTabs();
if (filterGroupCard()) {
if (currentTab.value == groupTabIndex) {
currentTab.value =
tabs.firstWhereOrNull((e) => e != groupTabIndex) ?? 0;
bind.setLocalFlutterConfig(
k: 'peer-tab-index', v: currentTab.value.toString());
}
} else {
if (gFFI.userModel.isAdmin.isFalse &&
gFFI.userModel.groupName.isNotEmpty) {
tabNames[groupTabIndex] = gFFI.userModel.groupName.value;
} else {
tabNames[groupTabIndex] = defaultGroupTabname;
}
if (tabs.contains(groupTabIndex) &&
int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ==
groupTabIndex) {
currentTab.value = groupTabIndex;
}
}
}
List<int> currentTabs() {
var v = List<int>.empty(growable: true);
for (int i = 0; i < tabNames.length; i++) {
if (!_isTabHidden(i) && !_isTabFilter(i)) {
v.add(i);
}
}
return v;
}
bool filterGroupCard() {
if (gFFI.groupModel.users.isEmpty ||
(gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
return true;
} else {
return false;
}
}
bool _isTabHidden(int tabindex) {
return tabHiddenFlag & (1 << tabindex) != 0;
}
bool _isTabFilter(int tabIndex) {
if (tabIndex == groupTabIndex) {
return filterGroupCard();
}
return false;
}
List<int> _notHiddenTabs() {
var v = List<int>.empty(growable: true);
for (int i = 0; i < tabNames.length; i++) {
if (!_isTabHidden(i)) {
v.add(i);
}
}
return v;
}
}
final statePeerTab = StatePeerTab.instance;
class PeerTabPage extends StatefulWidget {
const PeerTabPage({Key? key}) : super(key: key);
@override
@@ -156,11 +66,10 @@ class _PeerTabPageState extends State<PeerTabPage>
),
() => {}),
];
final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
@override
void initState() {
adjustTab();
final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type');
if (uiType != '') {
peerCardUiType.value = int.parse(uiType) == PeerUiType.list.index
@@ -172,16 +81,11 @@ class _PeerTabPageState extends State<PeerTabPage>
Future<void> handleTabSelection(int tabIndex) async {
if (tabIndex < entries.length) {
statePeerTab.currentTab.value = tabIndex;
gFFI.peerTabModel.setCurrentTab(tabIndex);
entries[tabIndex].load();
}
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
@@ -199,6 +103,7 @@ class _PeerTabPageState extends State<PeerTabPage>
Expanded(
child: visibleContextMenuListener(
_createSwitchBar(context))),
buildScrollJumper(),
const PeerSearchBar(),
Offstage(
offstage: !isDesktop,
@@ -213,82 +118,115 @@ class _PeerTabPageState extends State<PeerTabPage>
}
Widget _createSwitchBar(BuildContext context) {
final textColor = Theme.of(context).textTheme.titleLarge?.color;
return Obx(() {
var tabs = statePeerTab.currentTabs();
return ListView(
scrollDirection: Axis.horizontal,
physics: NeverScrollableScrollPhysics(),
controller: ScrollController(),
children: tabs.map((t) {
return InkWell(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: statePeerTab.currentTab.value == t
? Theme.of(context).backgroundColor
: null,
borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
),
child: Align(
alignment: Alignment.center,
child: Text(
translatedTabname(t),
textAlign: TextAlign.center,
style: TextStyle(
height: 1,
fontSize: 14,
color: statePeerTab.currentTab.value == t
? textColor
: textColor
?..withOpacity(0.5)),
),
)),
onTap: () async {
await handleTabSelection(t);
await bind.setLocalFlutterConfig(
k: 'peer-tab-index', v: t.toString());
final model = Provider.of<PeerTabModel>(context);
int indexCounter = -1;
return ReorderableListView(
buildDefaultDragHandles: false,
onReorder: (oldIndex, newIndex) {
model.onReorder(oldIndex, newIndex);
},
scrollDirection: Axis.horizontal,
physics: NeverScrollableScrollPhysics(),
scrollController: model.sc,
children: model.visibleOrderedTabs.map((t) {
indexCounter++;
return ReorderableDragStartListener(
key: ValueKey(t),
index: indexCounter,
child: VisibilityDetector(
key: ValueKey(t),
onVisibilityChanged: (info) {
final id = (info.key as ValueKey).value;
model.setTabFullyVisible(id, info.visibleFraction > 0.99);
},
);
}).toList());
});
child: Listener(
// handle mouse wheel
onPointerSignal: (e) {
if (e is PointerScrollEvent) {
if (!model.sc.canScroll) return;
_scrollDebounce.call(() {
model.sc.animateTo(model.sc.offset + e.scrollDelta.dy,
duration: Duration(milliseconds: 200),
curve: Curves.ease);
});
}
},
child: InkWell(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: model.currentTab == t
? Theme.of(context).backgroundColor
: null,
borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
),
child: Align(
alignment: Alignment.center,
child: Text(
model.translatedTabname(t),
textAlign: TextAlign.center,
style: TextStyle(
height: 1,
fontSize: 14,
color: model.currentTab == t
? MyTheme.tabbar(context).selectedTextColor
: MyTheme.tabbar(context).unSelectedTextColor
?..withOpacity(0.5)),
),
)),
onTap: () async {
await handleTabSelection(t);
await bind.setLocalFlutterConfig(
k: 'peer-tab-index', v: t.toString());
},
),
),
),
);
}).toList());
}
translatedTabname(int index) {
if (index < statePeerTab.tabNames.length) {
final name = statePeerTab.tabNames[index];
if (index == groupTabIndex) {
if (name == defaultGroupTabname) {
return translate(name);
} else {
return name;
}
} else {
return translate(name);
}
}
assert(false);
return index.toString();
Widget buildScrollJumper() {
final model = Provider.of<PeerTabModel>(context);
return Offstage(
offstage: !model.showScrollBtn,
child: Row(
children: [
GestureDetector(
child: Icon(Icons.arrow_left,
size: 22,
color: model.leftFullyVisible
? Theme.of(context).disabledColor
: null),
onTap: model.sc.backward),
GestureDetector(
child: Icon(Icons.arrow_right,
size: 22,
color: model.rightFullyVisible
? Theme.of(context).disabledColor
: null),
onTap: model.sc.forward)
],
));
}
Widget _createPeersView() {
final verticalMargin = isDesktop ? 12.0 : 6.0;
return Expanded(
child: Obx(() {
var tabs = statePeerTab.currentTabs();
if (tabs.isEmpty) {
return visibleContextMenuListener(Center(
child: Text(translate('Right click to select tabs')),
));
final model = Provider.of<PeerTabModel>(context);
Widget child;
if (model.visibleOrderedTabs.isEmpty) {
child = visibleContextMenuListener(Center(
child: Text(translate('Right click to select tabs')),
));
} else {
if (model.visibleOrderedTabs.contains(model.currentTab)) {
child = entries[model.currentTab].widget;
} else {
if (tabs.contains(statePeerTab.currentTab.value)) {
return entries[statePeerTab.currentTab.value].widget;
} else {
statePeerTab.currentTab.value = tabs[0];
return entries[statePeerTab.currentTab.value].widget;
}
model.setCurrentTab(model.visibleOrderedTabs[0]);
child = entries[0].widget;
}
}).marginSymmetric(vertical: verticalMargin));
}
return Expanded(
child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0));
}
Widget _createPeerViewTypeSwitch(BuildContext context) {
@@ -321,13 +259,6 @@ class _PeerTabPageState extends State<PeerTabPage>
);
}
adjustTab() {
var tabs = statePeerTab.currentTabs();
if (tabs.isNotEmpty && !tabs.contains(statePeerTab.currentTab.value)) {
statePeerTab.currentTab.value = tabs[0];
}
}
Widget visibleContextMenuListener(Widget child) {
return Listener(
onPointerDown: (e) {
@@ -347,44 +278,36 @@ class _PeerTabPageState extends State<PeerTabPage>
}
Widget visibleContextMenu(CancelFunc cancelFunc) {
return Obx(() {
final List<MenuEntryBase> menu = List.empty(growable: true);
for (int i = 0; i < statePeerTab.tabNames.length; i++) {
if (i == groupTabIndex && statePeerTab.filterGroupCard()) {
continue;
}
int bitMask = 1 << i;
menu.add(MenuEntrySwitch(
switchType: SwitchType.scheckbox,
text: translatedTabname(i),
getter: () async {
return statePeerTab.tabHiddenFlag & bitMask == 0;
},
setter: (show) async {
if (show) {
statePeerTab.tabHiddenFlag.value &= ~bitMask;
} else {
statePeerTab.tabHiddenFlag.value |= bitMask;
}
await bind.setLocalFlutterConfig(
k: 'hidden-peer-card',
v: statePeerTab.tabHiddenFlag.value.toRadixString(2));
cancelFunc();
adjustTab();
}));
}
return mod_menu.PopupMenu(
items: menu
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: MyTheme.accent,
height: 20.0,
dividerHeight: 12.0,
)))
.expand((i) => i)
.toList());
});
final model = Provider.of<PeerTabModel>(context);
final List<MenuEntryBase> menu = List.empty(growable: true);
final List<int> menuIndex = List.empty(growable: true);
var list = model.orderedNotFilteredTabs();
for (int i = 0; i < list.length; i++) {
int tabIndex = list[i];
int bitMask = 1 << tabIndex;
menuIndex.add(tabIndex);
menu.add(MenuEntrySwitch(
switchType: SwitchType.scheckbox,
text: model.translatedTabname(tabIndex),
getter: () async {
return model.tabHiddenFlag & bitMask == 0;
},
setter: (show) async {
model.onHideShow(tabIndex, show);
cancelFunc();
}));
}
return mod_menu.PopupMenu(
items: menu
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: MyTheme.accent,
height: 20.0,
dividerHeight: 12.0,
)))
.expand((i) => i)
.toList());
}
}
@@ -421,7 +344,9 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
FocusNode focusNode = FocusNode();
focusNode.addListener(() {
focused.value = focusNode.hasFocus;
peerSearchTextController.selection = TextSelection(baseOffset: 0, extentOffset: peerSearchTextController.value.text.length);
peerSearchTextController.selection = TextSelection(
baseOffset: 0,
extentOffset: peerSearchTextController.value.text.length);
});
return Container(
width: 120,

View File

@@ -1,17 +1,19 @@
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
const double kDesktopRemoteTabBarHeight = 28.0;
const int kMainWindowId = 0;
const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS";
const String kPeerPlatformAndroid = "Android";
/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page"
/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)', "Install Page"
const String kAppTypeMain = "main";
const String kAppTypeConnectionManager = "cm";
const String kAppTypeDesktopRemote = "remote";
const String kAppTypeDesktopFileTransfer = "file transfer";
const String kAppTypeDesktopPortForward = "port forward";
@@ -24,7 +26,6 @@ const String kWindowEventShow = "show";
const String kWindowConnect = "connect";
const String kUniLinksPrefix = "rustdesk://";
const String kActionNewConnection = "connection/new/";
const String kTabLabelHomePage = "Home";
const String kTabLabelSettingPage = "Settings";
@@ -105,6 +106,12 @@ const kRemoteImageQualityLow = 'low';
/// [kRemoteImageQualityCustom] Custom image quality.
const kRemoteImageQualityCustom = 'custom';
/// [kRemoteAudioGuestToHost] Guest to host audio mode(default).
const kRemoteAudioGuestToHost = 'guest-to-host';
/// [kRemoteAudioDualWay] dual-way audio mode(default).
const kRemoteAudioDualWay = 'dual-way';
const kIgnoreDpi = true;
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels

View File

@@ -44,6 +44,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
var watchIsCanScreenRecording = false;
var watchIsProcessTrust = false;
var watchIsInputMonitoring = false;
var watchIsCanRecordAudio = false;
Timer? _updateTimer;
@override
@@ -79,7 +80,16 @@ class _DesktopHomePageState extends State<DesktopHomePage>
buildTip(context),
buildIDBoard(context),
buildPasswordBoard(context),
buildHelpCards(),
FutureBuilder<Widget>(
future: buildHelpCards(),
builder: (_, data) {
if (data.hasData) {
return data.data!;
} else {
return const Offstage();
}
},
),
],
),
),
@@ -302,7 +312,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
);
}
Widget buildHelpCards() {
Future<Widget> buildHelpCards() async {
if (updateUrl.isNotEmpty) {
return buildInstallCard(
"Status",
@@ -349,6 +359,15 @@ class _DesktopHomePageState extends State<DesktopHomePage>
bind.mainIsInstalledDaemon(prompt: true);
});
}
//// Disable microphone configuration for macOS. We will request the permission when needed.
// else if ((await osxCanRecordAudio() !=
// PermissionAuthorizeType.authorized)) {
// return buildInstallCard("Permissions", "config_microphone", "Configure",
// () async {
// osxRequestAudio();
// watchIsCanRecordAudio = true;
// });
// }
} else if (Platform.isLinux) {
if (bind.mainCurrentIsWayland()) {
return buildInstallCard(
@@ -481,6 +500,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
setState(() {});
}
}
if (watchIsCanRecordAudio) {
if (Platform.isMacOS) {
Future.microtask(() async {
if ((await osxCanRecordAudio() ==
PermissionAuthorizeType.authorized)) {
watchIsCanRecordAudio = false;
setState(() {});
}
});
} else {
watchIsCanRecordAudio = false;
setState(() {});
}
}
});
Get.put<RxBool>(svcStopped, tag: 'stop-service');
rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);

View File

@@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_custom_cursor/cursor_manager.dart'
@@ -376,10 +375,10 @@ class _RemotePageState extends State<RemotePage>
class ImagePaint extends StatefulWidget {
final String id;
final Rx<bool> zoomCursor;
final Rx<bool> cursorOverImage;
final Rx<bool> keyboardEnabled;
final Rx<bool> remoteCursorMoved;
final RxBool zoomCursor;
final RxBool cursorOverImage;
final RxBool keyboardEnabled;
final RxBool remoteCursorMoved;
final Widget Function(Widget)? listenerBuilder;
ImagePaint(
@@ -402,10 +401,10 @@ class _ImagePaintState extends State<ImagePaint> {
final ScrollController _vertical = ScrollController();
String get id => widget.id;
Rx<bool> get zoomCursor => widget.zoomCursor;
Rx<bool> get cursorOverImage => widget.cursorOverImage;
Rx<bool> get keyboardEnabled => widget.keyboardEnabled;
Rx<bool> get remoteCursorMoved => widget.remoteCursorMoved;
RxBool get zoomCursor => widget.zoomCursor;
RxBool get cursorOverImage => widget.cursorOverImage;
RxBool get keyboardEnabled => widget.keyboardEnabled;
RxBool get remoteCursorMoved => widget.remoteCursorMoved;
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
@override
@@ -414,27 +413,50 @@ class _ImagePaintState extends State<ImagePaint> {
var c = Provider.of<CanvasModel>(context);
final s = c.scale;
mouseRegion({child}) => Obx(() => MouseRegion(
cursor: cursorOverImage.isTrue
? c.cursorEmbedded
? SystemMouseCursors.none
: keyboardEnabled.isTrue
? (() {
if (remoteCursorMoved.isTrue) {
_lastRemoteCursorMoved = true;
return SystemMouseCursors.none;
} else {
if (_lastRemoteCursorMoved) {
_lastRemoteCursorMoved = false;
_firstEnterImage.value = true;
}
return _buildCustomCursor(context, s);
}
}())
: _buildDisabledCursor(context, s)
: MouseCursor.defer,
onHover: (evt) {},
child: child));
mouseRegion({child}) => Obx(() {
double getCursorScale() {
var c = Provider.of<CanvasModel>(context);
var cursorScale = 1.0;
if (Platform.isWindows) {
// debug win10
final isViewAdaptive =
c.viewStyle.style == kRemoteViewStyleAdaptive;
if (zoomCursor.value && isViewAdaptive) {
cursorScale = s * c.devicePixelRatio;
}
} else {
final isViewOriginal =
c.viewStyle.style == kRemoteViewStyleOriginal;
if (zoomCursor.value || isViewOriginal) {
cursorScale = s;
}
}
return cursorScale;
}
return MouseRegion(
cursor: cursorOverImage.isTrue
? c.cursorEmbedded
? SystemMouseCursors.none
: keyboardEnabled.isTrue
? (() {
if (remoteCursorMoved.isTrue) {
_lastRemoteCursorMoved = true;
return SystemMouseCursors.none;
} else {
if (_lastRemoteCursorMoved) {
_lastRemoteCursorMoved = false;
_firstEnterImage.value = true;
}
return _buildCustomCursor(
context, getCursorScale());
}
}())
: _buildDisabledCursor(context, getCursorScale())
: MouseCursor.defer,
onHover: (evt) {},
child: child);
});
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
final imageWidth = c.getDisplayWidth() * s;
@@ -480,7 +502,7 @@ class _ImagePaintState extends State<ImagePaint> {
if (cache == null) {
return MouseCursor.defer;
} else {
final key = cache.updateGetKey(scale, zoomCursor.value);
final key = cache.updateGetKey(scale);
if (!cursor.cachedKeys.contains(key)) {
debugPrint("Register custom cursor with key $key");
// [Safety]
@@ -646,7 +668,8 @@ class CursorPaint extends StatelessWidget {
double x = (m.x - hotx) * c.scale + cx;
double y = (m.y - hoty) * c.scale + cy;
double scale = 1.0;
if (zoomCursor.isTrue) {
final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal;
if (zoomCursor.value || isViewOriginal) {
x = m.x - hotx + cx / c.scale;
y = m.y - hoty + cy / c.scale;
scale = c.scale;

View File

@@ -243,96 +243,35 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
padding: padding,
),
MenuEntryDivider<String>(),
MenuEntryRadios<String>(
text: translate('Ratio'),
optionsGetter: () => [
MenuEntryRadioOption(
text: translate('Scale original'),
value: kRemoteViewStyleOriginal,
dismissOnClicked: true,
),
MenuEntryRadioOption(
text: translate('Scale adaptive'),
value: kRemoteViewStyleAdaptive,
dismissOnClicked: true,
),
],
curOptionGetter: () async =>
// null means peer id is not found, which there's no need to care about
await bind.sessionGetViewStyle(id: key) ?? '',
optionSetter: (String oldValue, String newValue) async {
await bind.sessionSetViewStyle(id: key, value: newValue);
ffi.canvasModel.updateViewStyle();
cancelFunc();
},
padding: padding,
RemoteMenuEntry.viewStyle(
key,
ffi,
padding,
dismissFunc: cancelFunc,
),
]);
if (!ffi.canvasModel.cursorEmbedded) {
menu.add(MenuEntryDivider<String>());
menu.add(() {
final state = ShowRemoteCursorState.find(key);
return MenuEntrySwitch2<String>(
switchType: SwitchType.scheckbox,
text: translate('Show remote cursor'),
getter: () {
return state;
},
setter: (bool v) async {
state.value = v;
await bind.sessionToggleOption(
id: key, value: 'show-remote-cursor');
cancelFunc();
},
padding: padding,
);
}());
menu.add(RemoteMenuEntry.showRemoteCursor(
key,
padding,
dismissFunc: cancelFunc,
));
}
if (perms['keyboard'] != false) {
if (perms['clipboard'] != false) {
menu.add(MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox,
text: translate('Disable clipboard'),
getter: () async {
return bind.sessionGetToggleOptionSync(
id: key, arg: 'disable-clipboard');
},
setter: (bool v) async {
await bind.sessionToggleOption(id: key, value: 'disable-clipboard');
cancelFunc();
},
padding: padding,
));
menu.add(RemoteMenuEntry.disableClipboard(key, padding,
dismissFunc: cancelFunc));
}
menu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Insert Lock'),
style: style,
),
proc: () {
bind.sessionLockScreen(id: key);
cancelFunc();
},
padding: padding,
dismissOnClicked: true,
));
menu.add(
RemoteMenuEntry.insertLock(key, padding, dismissFunc: cancelFunc));
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
menu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
'${translate("Insert")} Ctrl + Alt + Del',
style: style,
),
proc: () {
bind.sessionCtrlAltDel(id: key);
cancelFunc();
},
padding: padding,
dismissOnClicked: true,
));
menu.add(RemoteMenuEntry.insertCtrlAltDel(key, padding,
dismissFunc: cancelFunc));
}
}

View File

@@ -514,6 +514,39 @@ class _CmControlPanel extends StatelessWidget {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Offstage(
offstage: !client.inVoiceCall,
child: buildButton(context,
color: Colors.red,
onClick: () => closeVoiceCall(),
icon: Icon(Icons.phone_disabled_rounded, color: Colors.white),
text: "Stop voice call",
textColor: Colors.white),
),
Offstage(
offstage: !client.incomingVoiceCall,
child: Row(
children: [
Expanded(
child: buildButton(context,
color: MyTheme.accent,
onClick: () => handleVoiceCall(true),
icon: Icon(Icons.phone_enabled, color: Colors.white),
text: "Accept",
textColor: Colors.white),
),
Expanded(
child: buildButton(context,
color: Colors.red,
onClick: () => handleVoiceCall(false),
icon:
Icon(Icons.phone_disabled_rounded, color: Colors.white),
text: "Dismiss",
textColor: Colors.white),
)
],
),
),
Offstage(
offstage: !client.fromSwitch,
child: buildButton(context,
@@ -619,7 +652,7 @@ class _CmControlPanel extends StatelessWidget {
.marginSymmetric(horizontal: showElevation ? 0 : bigMargin);
}
buildButton(
Widget buildButton(
BuildContext context, {
required Color? color,
required Function() onClick,
@@ -685,6 +718,14 @@ class _CmControlPanel extends StatelessWidget {
void handleSwitchBack(BuildContext context) {
bind.cmSwitchBack(connId: client.id);
}
void handleVoiceCall(bool accept) {
bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept);
}
void closeVoiceCall() {
bind.cmCloseVoiceCall(id: client.id);
}
}
void checkClickTime(int id, Function() callback) async {

View File

@@ -790,6 +790,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
_PopupMenuRoute({
required this.position,
required this.items,
this.menuWrapper,
this.initialValue,
this.elevation,
required this.barrierLabel,
@@ -802,6 +803,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
final RelativeRect position;
final List<PopupMenuEntry<T>> items;
final MenuWrapper? menuWrapper;
final List<Size?> itemSizes;
final T? initialValue;
final double? elevation;
@@ -844,11 +846,14 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
}
}
final Widget menu = _PopupMenu<T>(
Widget menu = _PopupMenu<T>(
route: this,
semanticLabel: semanticLabel,
constraints: constraints,
);
if (this.menuWrapper != null) {
menu = this.menuWrapper!(menu);
}
final MediaQueryData mediaQuery = MediaQuery.of(context);
return MediaQuery.removePadding(
context: context,
@@ -1035,6 +1040,7 @@ Future<T?> showMenu<T>({
required BuildContext context,
required RelativeRect position,
required List<PopupMenuEntry<T>> items,
MenuWrapper? menuWrapper,
T? initialValue,
double? elevation,
String? semanticLabel,
@@ -1062,6 +1068,7 @@ Future<T?> showMenu<T>({
return navigator.push(_PopupMenuRoute<T>(
position: position,
items: items,
menuWrapper: menuWrapper,
initialValue: initialValue,
elevation: elevation,
semanticLabel: semanticLabel,
@@ -1094,6 +1101,8 @@ typedef PopupMenuCanceled = void Function();
typedef PopupMenuItemBuilder<T> = List<PopupMenuEntry<T>> Function(
BuildContext context);
typedef MenuWrapper = Widget Function(Widget child);
/// Displays a menu when pressed and calls [onSelected] when the menu is dismissed
/// because an item was selected. The value passed to [onSelected] is the value of
/// the selected menu item.
@@ -1124,6 +1133,7 @@ class PopupMenuButton<T> extends StatefulWidget {
const PopupMenuButton({
Key? key,
required this.itemBuilder,
this.menuWrapper,
this.initialValue,
this.onHover,
this.onSelected,
@@ -1151,6 +1161,9 @@ class PopupMenuButton<T> extends StatefulWidget {
/// Called when the button is pressed to create the items to show in the menu.
final PopupMenuItemBuilder<T> itemBuilder;
/// Menu wrapper.
final MenuWrapper? menuWrapper;
/// The value of the menu item, if any, that should be highlighted when the menu opens.
final T? initialValue;
@@ -1333,6 +1346,7 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
context: context,
elevation: widget.elevation ?? popupMenuTheme.elevation,
items: items,
menuWrapper: widget.menuWrapper,
initialValue: widget.initialValue,
position: position,
shape: widget.shape ?? popupMenuTheme.shape,

View File

@@ -109,13 +109,17 @@ class MenuConfig {
this.boxWidth});
}
typedef DismissCallback = Function();
abstract class MenuEntryBase<T> {
bool dismissOnClicked;
DismissCallback? dismissCallback;
RxBool? enabled;
MenuEntryBase({
this.dismissOnClicked = false,
this.enabled,
this.dismissCallback,
});
List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf);
@@ -146,12 +150,14 @@ class MenuEntryRadioOption {
String value;
bool dismissOnClicked;
RxBool? enabled;
DismissCallback? dismissCallback;
MenuEntryRadioOption({
required this.text,
required this.value,
this.dismissOnClicked = false,
this.enabled,
this.dismissCallback,
});
}
@@ -177,8 +183,13 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
required this.optionSetter,
this.padding,
dismissOnClicked = false,
dismissCallback,
RxBool? enabled,
}) : super(dismissOnClicked: dismissOnClicked, enabled: enabled) {
}) : super(
dismissOnClicked: dismissOnClicked,
enabled: enabled,
dismissCallback: dismissCallback,
) {
() async {
_curOption.value = await curOptionGetter();
}();
@@ -249,6 +260,9 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
onPressed() {
if (opt.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
if (opt.dismissCallback != null) {
opt.dismissCallback!();
}
}
setOption(opt.value);
}
@@ -360,6 +374,9 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
onPressed: () {
if (opt.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
if (opt.dismissCallback != null) {
opt.dismissCallback!();
}
}
setOption(opt.value);
},
@@ -421,7 +438,12 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
this.textStyle,
this.padding,
RxBool? enabled,
}) : super(dismissOnClicked: dismissOnClicked, enabled: enabled);
dismissCallback,
}) : super(
dismissOnClicked: dismissOnClicked,
enabled: enabled,
dismissCallback: dismissCallback,
);
RxBool get curOption;
Future<void> setOption(bool? option);
@@ -463,6 +485,9 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
if (super.dismissOnClicked &&
Navigator.canPop(context)) {
Navigator.pop(context);
if (super.dismissCallback != null) {
super.dismissCallback!();
}
}
setOption(v);
},
@@ -474,6 +499,9 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
if (super.dismissOnClicked &&
Navigator.canPop(context)) {
Navigator.pop(context);
if (super.dismissCallback != null) {
super.dismissCallback!();
}
}
setOption(v);
},
@@ -485,6 +513,9 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
onPressed: () {
if (super.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
if (super.dismissCallback != null) {
super.dismissCallback!();
}
}
setOption(!curOption.value);
},
@@ -508,6 +539,7 @@ class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
EdgeInsets? padding,
dismissOnClicked = false,
RxBool? enabled,
dismissCallback,
}) : super(
switchType: switchType,
text: text,
@@ -515,6 +547,7 @@ class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
padding: padding,
dismissOnClicked: dismissOnClicked,
enabled: enabled,
dismissCallback: dismissCallback,
) {
() async {
_curOption.value = await getter();
@@ -551,12 +584,15 @@ class MenuEntrySwitch2<T> extends MenuEntrySwitchBase<T> {
EdgeInsets? padding,
dismissOnClicked = false,
RxBool? enabled,
dismissCallback,
}) : super(
switchType: switchType,
text: text,
textStyle: textStyle,
padding: padding,
dismissOnClicked: dismissOnClicked);
switchType: switchType,
text: text,
textStyle: textStyle,
padding: padding,
dismissOnClicked: dismissOnClicked,
dismissCallback: dismissCallback,
);
@override
RxBool get curOption => getter();
@@ -627,9 +663,11 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
this.padding,
dismissOnClicked = false,
RxBool? enabled,
dismissCallback,
}) : super(
dismissOnClicked: dismissOnClicked,
enabled: enabled,
dismissCallback: dismissCallback,
);
Widget _buildChild(BuildContext context, MenuConfig conf) {
@@ -641,6 +679,9 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
? () {
if (super.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
if (super.dismissCallback != null) {
super.dismissCallback!();
}
}
proc();
}

View File

@@ -9,6 +9,7 @@ import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:debounce_throttle/debounce_throttle.dart';
@@ -99,6 +100,175 @@ class _MenubarTheme {
static const double dividerHeight = 12.0;
}
typedef DismissFunc = void Function();
class RemoteMenuEntry {
static MenuEntryRadios<String> viewStyle(
String remoteId,
FFI ffi,
EdgeInsets padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
RxString? rxViewStyle,
}) {
return MenuEntryRadios<String>(
text: translate('Ratio'),
optionsGetter: () => [
MenuEntryRadioOption(
text: translate('Scale original'),
value: kRemoteViewStyleOriginal,
dismissOnClicked: true,
dismissCallback: dismissCallback,
),
MenuEntryRadioOption(
text: translate('Scale adaptive'),
value: kRemoteViewStyleAdaptive,
dismissOnClicked: true,
dismissCallback: dismissCallback,
),
],
curOptionGetter: () async {
// null means peer id is not found, which there's no need to care about
final viewStyle = await bind.sessionGetViewStyle(id: remoteId) ?? '';
if (rxViewStyle != null) {
rxViewStyle.value = viewStyle;
}
return viewStyle;
},
optionSetter: (String oldValue, String newValue) async {
await bind.sessionSetViewStyle(id: remoteId, value: newValue);
if (rxViewStyle != null) {
rxViewStyle.value = newValue;
}
ffi.canvasModel.updateViewStyle();
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: true,
dismissCallback: dismissCallback,
);
}
static MenuEntrySwitch2<String> showRemoteCursor(
String remoteId,
EdgeInsets padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
final state = ShowRemoteCursorState.find(remoteId);
final optKey = 'show-remote-cursor';
return MenuEntrySwitch2<String>(
switchType: SwitchType.scheckbox,
text: translate('Show remote cursor'),
getter: () {
return state;
},
setter: (bool v) async {
await bind.sessionToggleOption(id: remoteId, value: optKey);
state.value =
bind.sessionGetToggleOptionSync(id: remoteId, arg: optKey);
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: true,
dismissCallback: dismissCallback,
);
}
static MenuEntrySwitch<String> disableClipboard(
String remoteId,
EdgeInsets? padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
return createSwitchMenuEntry(
remoteId,
'Disable clipboard',
'disable-clipboard',
padding,
true,
dismissCallback: dismissCallback,
);
}
static MenuEntrySwitch<String> createSwitchMenuEntry(
String remoteId,
String text,
String option,
EdgeInsets? padding,
bool dismissOnClicked, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
return MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox,
text: translate(text),
getter: () async {
return bind.sessionGetToggleOptionSync(id: remoteId, arg: option);
},
setter: (bool v) async {
await bind.sessionToggleOption(id: remoteId, value: option);
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: dismissOnClicked,
dismissCallback: dismissCallback,
);
}
static MenuEntryButton<String> insertLock(
String remoteId,
EdgeInsets? padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Insert Lock'),
style: style,
),
proc: () {
bind.sessionLockScreen(id: remoteId);
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: true,
dismissCallback: dismissCallback,
);
}
static insertCtrlAltDel(
String remoteId,
EdgeInsets? padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
'${translate("Insert")} Ctrl + Alt + Del',
style: style,
),
proc: () {
bind.sessionCtrlAltDel(id: remoteId);
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: true,
dismissCallback: dismissCallback,
);
}
}
class RemoteMenubar extends StatefulWidget {
final String id;
final FFI ffi;
@@ -221,6 +391,18 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}
}
Widget _buildPointerTrackWidget(Widget child) {
return Listener(
onPointerHover: (PointerHoverEvent e) =>
widget.ffi.inputModel.lastMousePos = e.position,
child: MouseRegion(
child: child,
),
);
}
_menuDismissCallback() => widget.ffi.inputModel.refreshMousePos();
Widget _buildMenubar(BuildContext context) {
final List<Widget> menubarItems = [];
if (!isWebDesktop) {
@@ -244,6 +426,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
menubarItems.add(_buildKeyboard(context));
if (!isWeb) {
menubarItems.add(_buildChat(context));
menubarItems.add(_buildVoiceCall(context));
}
menubarItems.add(_buildRecording(context));
menubarItems.add(_buildClose(context));
@@ -297,31 +480,6 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
);
}
final _chatButtonKey = GlobalKey();
Widget _buildChat(BuildContext context) {
return IconButton(
key: _chatButtonKey,
tooltip: translate('Chat'),
onPressed: () {
RenderBox? renderBox =
_chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
Offset? initPos;
if (renderBox != null) {
final pos = renderBox.localToGlobal(Offset.zero);
initPos = Offset(pos.dx, pos.dy + _MenubarTheme.dividerHeight);
}
widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
},
icon: const Icon(
Icons.message,
color: _MenubarTheme.commonColor,
),
);
}
Widget _buildMonitor(BuildContext context) {
final pi = widget.ffi.ffiModel.pi;
return mod_menu.PopupMenuButton(
@@ -375,6 +533,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
_menuDismissCallback();
}
RxInt display = CurrentDisplayState.find(widget.id);
if (display.value != i) {
@@ -390,13 +549,10 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
mod_menu.PopupMenuItem<String>(
height: _MenubarTheme.height,
padding: EdgeInsets.zero,
child: Listener(
onPointerHover: (PointerHoverEvent e) =>
widget.ffi.inputModel.lastMousePos = e.position,
child: MouseRegion(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: rowChildren),
child: _buildPointerTrackWidget(
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: rowChildren,
),
),
)
@@ -446,6 +602,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
),
tooltip: translate('Display Settings'),
position: mod_menu.PopupMenuPosition.under,
menuWrapper: _buildPointerTrackWidget,
itemBuilder: (BuildContext context) =>
_getDisplayMenu(snapshot.data!, remoteCount)
.map((entry) => entry.build(
@@ -500,12 +657,17 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
? translate('Stop session recording')
: translate('Start session recording'),
onPressed: () => value.toggle(),
icon: Icon(
value.start
? Icons.pause_circle_filled
: Icons.videocam_outlined,
color: _MenubarTheme.commonColor,
),
icon: value.start
? Icon(
Icons.pause_circle_filled,
color: _MenubarTheme.commonColor,
)
: SvgPicture.asset(
"assets/record_screen.svg",
color: _MenubarTheme.commonColor,
width: Theme.of(context).iconTheme.size ?? 22.0,
height: Theme.of(context).iconTheme.size ?? 22.0,
),
));
} else {
return Offstage();
@@ -526,6 +688,130 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
);
}
final _chatButtonKey = GlobalKey();
Widget _buildChat(BuildContext context) {
FfiModel ffiModel = Provider.of<FfiModel>(context);
return mod_menu.PopupMenuButton(
key: _chatButtonKey,
padding: EdgeInsets.zero,
icon: SvgPicture.asset(
"assets/chat.svg",
color: _MenubarTheme.commonColor,
width: Theme.of(context).iconTheme.size ?? 24.0,
height: Theme.of(context).iconTheme.size ?? 24.0,
),
tooltip: translate('Chat'),
position: mod_menu.PopupMenuPosition.under,
itemBuilder: (BuildContext context) => _getChatMenu(context)
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: _MenubarTheme.commonColor,
height: _MenubarTheme.height,
dividerHeight: _MenubarTheme.dividerHeight,
)))
.expand((i) => i)
.toList(),
);
}
Widget _getVoiceCallIcon() {
switch (widget.ffi.chatModel.voiceCallStatus.value) {
case VoiceCallStatus.waitingForResponse:
return IconButton(
onPressed: () {
widget.ffi.chatModel.closeVoiceCall(widget.id);
},
icon: SvgPicture.asset(
"assets/voice_call_waiting.svg",
color: Colors.red,
width: Theme.of(context).iconTheme.size ?? 20.0,
height: Theme.of(context).iconTheme.size ?? 20.0,
));
case VoiceCallStatus.connected:
return IconButton(
onPressed: () {
widget.ffi.chatModel.closeVoiceCall(widget.id);
},
icon: Icon(
Icons.phone_disabled_rounded,
color: Colors.red,
size: Theme.of(context).iconTheme.size ?? 22.0,
),
);
default:
return const Offstage();
}
}
String? _getVoiceCallTooltip() {
switch (widget.ffi.chatModel.voiceCallStatus.value) {
case VoiceCallStatus.waitingForResponse:
return "Waiting";
case VoiceCallStatus.connected:
return "Disconnect";
default:
return null;
}
}
Widget _buildVoiceCall(BuildContext context) {
return Obx(
() {
final tooltipText = _getVoiceCallTooltip();
return tooltipText == null
? const Offstage()
: IconButton(
padding: EdgeInsets.zero,
icon: _getVoiceCallIcon(),
tooltip: translate(tooltipText),
onPressed: () => bind.sessionRequestVoiceCall(id: widget.id),
);
},
);
}
List<MenuEntryBase<String>> _getChatMenu(BuildContext context) {
final List<MenuEntryBase<String>> chatMenu = [];
const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0);
chatMenu.addAll([
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Text chat'),
style: style,
),
proc: () {
RenderBox? renderBox =
_chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
Offset? initPos;
if (renderBox != null) {
final pos = renderBox.localToGlobal(Offset.zero);
initPos = Offset(pos.dx, pos.dy + _MenubarTheme.dividerHeight);
}
widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
},
padding: padding,
dismissOnClicked: true,
),
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Voice call'),
style: style,
),
proc: () {
// Request a voice call.
bind.sessionRequestVoiceCall(id: widget.id);
},
padding: padding,
dismissOnClicked: true,
),
]);
return chatMenu;
}
List<MenuEntryBase<String>> _getControlMenu(BuildContext context) {
final pi = widget.ffi.ffiModel.pi;
final perms = widget.ffi.ffiModel.permissions;
@@ -554,6 +840,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
_menuDismissCallback();
}
showSetOSPassword(
widget.id, false, widget.ffi.dialogManager);
@@ -566,6 +853,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
@@ -577,6 +865,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
@@ -588,6 +877,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
connect(context, widget.id, isTcpTunneling: true);
},
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
]);
// {handler.get_audit_server() && <li #note>{translate('Note')}</li>}
@@ -605,23 +895,15 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
);
}
displayMenu.add(MenuEntryDivider());
if (perms['keyboard'] != false) {
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
'${translate("Insert")} Ctrl + Alt + Del',
style: style,
),
proc: () {
bind.sessionCtrlAltDel(id: widget.id);
},
padding: padding,
dismissOnClicked: true,
));
displayMenu.add(RemoteMenuEntry.insertCtrlAltDel(widget.id, padding,
dismissCallback: _menuDismissCallback));
}
}
if (perms['restart'] != false &&
@@ -638,21 +920,13 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
if (perms['keyboard'] != false) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Insert Lock'),
style: style,
),
proc: () {
bind.sessionLockScreen(id: widget.id);
},
padding: padding,
dismissOnClicked: true,
));
displayMenu.add(RemoteMenuEntry.insertLock(widget.id, padding,
dismissCallback: _menuDismissCallback));
if (pi.platform == kPeerPlatformWindows) {
displayMenu.add(MenuEntryButton<String>(
@@ -670,6 +944,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
if (pi.platform != kPeerPlatformAndroid &&
@@ -684,6 +959,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
showConfirmSwitchSidesDialog(widget.id, widget.ffi.dialogManager),
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
}
@@ -699,6 +975,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
@@ -720,10 +997,10 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
// },
// padding: padding,
// dismissOnClicked: true,
// dismissCallback: _menuDismissCallback,
// ));
// }
}
return displayMenu;
}
@@ -758,33 +1035,12 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0);
final peer_version = widget.ffi.ffiModel.pi.version;
final displayMenu = [
MenuEntryRadios<String>(
text: translate('Ratio'),
optionsGetter: () => [
MenuEntryRadioOption(
text: translate('Scale original'),
value: kRemoteViewStyleOriginal,
dismissOnClicked: true,
),
MenuEntryRadioOption(
text: translate('Scale adaptive'),
value: kRemoteViewStyleAdaptive,
dismissOnClicked: true,
),
],
curOptionGetter: () async {
// null means peer id is not found, which there's no need to care about
final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? '';
widget.state.viewStyle.value = viewStyle;
return viewStyle;
},
optionSetter: (String oldValue, String newValue) async {
await bind.sessionSetViewStyle(id: widget.id, value: newValue);
widget.state.viewStyle.value = newValue;
widget.ffi.canvasModel.updateViewStyle();
},
padding: padding,
dismissOnClicked: true,
RemoteMenuEntry.viewStyle(
widget.id,
widget.ffi,
padding,
dismissCallback: _menuDismissCallback,
rxViewStyle: widget.state.viewStyle,
),
MenuEntryDivider<String>(),
MenuEntryRadios<String>(
@@ -794,21 +1050,26 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
text: translate('Good image quality'),
value: kRemoteImageQualityBest,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
MenuEntryRadioOption(
text: translate('Balanced'),
value: kRemoteImageQualityBalanced,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
MenuEntryRadioOption(
text: translate('Optimize reaction time'),
value: kRemoteImageQualityLow,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
MenuEntryRadioOption(
text: translate('Custom'),
value: kRemoteImageQualityCustom,
dismissOnClicked: true),
text: translate('Custom'),
value: kRemoteImageQualityCustom,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
],
curOptionGetter: () async =>
// null means peer id is not found, which there's no need to care about
@@ -973,12 +1234,14 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
text: translate('ScrollAuto'),
value: kRemoteScrollStyleAuto,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
enabled: widget.ffi.canvasModel.imageOverflow,
),
MenuEntryRadioOption(
text: translate('Scrollbar'),
value: kRemoteScrollStyleBar,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
enabled: widget.ffi.canvasModel.imageOverflow,
),
],
@@ -991,6 +1254,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
displayMenu.insert(3, MenuEntryDivider<String>());
@@ -1061,6 +1325,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
);
}
@@ -1087,11 +1352,13 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
text: translate('Auto'),
value: 'auto',
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
MenuEntryRadioOption(
text: 'VP9',
value: 'vp9',
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
];
if (codecs[0]) {
@@ -1099,6 +1366,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
text: 'H264',
value: 'h264',
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
if (codecs[1]) {
@@ -1106,6 +1374,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
text: 'H265',
value: 'h265',
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
return list;
@@ -1122,6 +1391,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
}
@@ -1129,23 +1399,11 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
/// Show remote cursor
if (!widget.ffi.canvasModel.cursorEmbedded) {
displayMenu.add(() {
final state = ShowRemoteCursorState.find(widget.id);
return MenuEntrySwitch2<String>(
switchType: SwitchType.scheckbox,
text: translate('Show remote cursor'),
getter: () {
return state;
},
setter: (bool v) async {
state.value = v;
await bind.sessionToggleOption(
id: widget.id, value: 'show-remote-cursor');
},
padding: padding,
dismissOnClicked: true,
);
}());
displayMenu.add(RemoteMenuEntry.showRemoteCursor(
widget.id,
padding,
dismissCallback: _menuDismissCallback,
));
}
/// Show remote cursor scaling with image
@@ -1160,11 +1418,13 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
return state;
},
setter: (bool v) async {
state.value = v;
await bind.sessionToggleOption(id: widget.id, value: opt);
state.value =
bind.sessionGetToggleOptionSync(id: widget.id, arg: opt);
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
);
}());
}
@@ -1184,6 +1444,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
final perms = widget.ffi.ffiModel.permissions;
@@ -1192,6 +1453,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
if (perms['audio'] != false) {
displayMenu
.add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true));
displayMenu
.add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true));
}
if (Platform.isWindows &&
@@ -1203,8 +1466,11 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
if (perms['keyboard'] != false) {
if (perms['clipboard'] != false) {
displayMenu.add(_createSwitchMenuEntry(
'Disable clipboard', 'disable-clipboard', padding, true));
displayMenu.add(RemoteMenuEntry.disableClipboard(
widget.id,
padding,
dismissCallback: _menuDismissCallback,
));
}
displayMenu.add(_createSwitchMenuEntry(
'Lock after session end', 'lock-after-session-end', padding, true));
@@ -1221,6 +1487,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
}
@@ -1233,25 +1500,29 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
text: translate('Ratio'),
optionsGetter: () {
List<MenuEntryRadioOption> list = [];
List<String> modes = ["legacy"];
List<KeyboardModeMenu> modes = [
KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'),
KeyboardModeMenu(key: 'map', menu: 'Map mode'),
KeyboardModeMenu(key: 'translate', menu: 'Translate mode'),
];
if (bind.sessionIsKeyboardModeSupported(id: widget.id, mode: "map")) {
modes.add("map");
}
for (String mode in modes) {
if (mode == "legacy") {
for (KeyboardModeMenu mode in modes) {
if (bind.sessionIsKeyboardModeSupported(
id: widget.id, mode: mode.key)) {
if (mode.key == 'translate') {
if (!Platform.isWindows ||
widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
continue;
}
}
list.add(MenuEntryRadioOption(
text: translate('Legacy mode'), value: 'legacy'));
} else if (mode == "map") {
list.add(MenuEntryRadioOption(
text: translate('Map mode'), value: 'map'));
text: translate(mode.menu), value: mode.key));
}
}
return list;
},
curOptionGetter: () async {
return await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy";
return await bind.sessionGetKeyboardMode(id: widget.id) ?? 'legacy';
},
optionSetter: (String oldValue, String newValue) async {
await bind.sessionSetKeyboardMode(id: widget.id, value: newValue);
@@ -1292,6 +1563,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
_menuDismissCallback();
}
showKBLayoutTypeChooser(
localPlatform, widget.ffi.dialogManager);
@@ -1304,6 +1576,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
proc: () {},
padding: EdgeInsets.zero,
dismissOnClicked: false,
dismissCallback: _menuDismissCallback,
),
);
}
@@ -1312,18 +1585,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
MenuEntrySwitch<String> _createSwitchMenuEntry(
String text, String option, EdgeInsets? padding, bool dismissOnClicked) {
return MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox,
text: translate(text),
getter: () async {
return bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
},
setter: (bool v) async {
await bind.sessionToggleOption(id: widget.id, value: option);
},
padding: padding,
dismissOnClicked: dismissOnClicked,
);
return RemoteMenuEntry.createSwitchMenuEntry(
widget.id, text, option, padding, dismissOnClicked,
dismissCallback: _menuDismissCallback);
}
}
@@ -1547,3 +1811,10 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
);
}
}
class KeyboardModeMenu {
final String key;
final String menu;
KeyboardModeMenu({required this.key, required this.menu});
}

View File

@@ -1,23 +1,23 @@
import 'dart:io';
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:ui' as ui;
import 'package:bot_toast/bot_toast.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide TabBarTheme;
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:scroll_pos/scroll_pos.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:bot_toast/bot_toast.dart';
import '../../utils/multi_window_manager.dart';
@@ -545,7 +545,9 @@ class WindowActionPanelState extends State<WindowActionPanel>
void onWindowClose() async {
// hide window on close
if (widget.isMainWindow) {
await rustDeskWinManager.unregisterActiveWindow(0);
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
}
// `hide` must be placed after unregisterActiveWindow, because once all windows are hidden,
// flutter closes the application on macOS. We should ensure the post-run logic has ran successfully.
// e.g.: saving window position.
@@ -976,7 +978,7 @@ class _CloseButton extends StatelessWidget {
offstage: !visible,
child: InkWell(
hoverColor: MyTheme.tabbar(context).closeHoverColor,
customBorder: const RoundedRectangleBorder(),
customBorder: const CircleBorder(),
onTap: () => onClose(),
child: Icon(
Icons.close,
@@ -1099,7 +1101,7 @@ class TabbarTheme extends ThemeExtension<TabbarTheme> {
unSelectedIconColor: Color.fromARGB(255, 96, 96, 96),
dividerColor: Color.fromARGB(255, 238, 238, 238),
hoverColor: Color.fromARGB(51, 158, 158, 158),
closeHoverColor: Colors.black,
closeHoverColor: Color.fromARGB(255, 224, 224, 224),
selectedTabBackgroundColor: Color.fromARGB(255, 240, 240, 240));
static const dark = TabbarTheme(

View File

@@ -1,22 +1,23 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:bot_toast/bot_toast.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/pages/server_page.dart';
import 'package:flutter_hbb/desktop/pages/install_page.dart';
import 'package:flutter_hbb/desktop/pages/server_page.dart';
import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
import 'package:bot_toast/bot_toast.dart';
// import 'package:window_manager/window_manager.dart';
@@ -31,6 +32,9 @@ int? kWindowId;
WindowType? kWindowType;
late List<String> kBootArgs;
/// Uni links.
StreamSubscription? _uniLinkSubscription;
Future<void> main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
debugPrint("launch args: $args");
@@ -114,7 +118,6 @@ Future<void> initEnv(String appType) async {
void runMainApp(bool startService) async {
// register uni links
initUniLinks();
await initEnv(kAppTypeMain);
// trigger connection status updater
await bind.mainCheckConnectStatus();
@@ -130,7 +133,11 @@ void runMainApp(bool startService) async {
// Restore the location of the main window before window hide or show.
await restoreWindowPosition(WindowType.Main);
// Check the startup argument, if we successfully handle the argument, we keep the main window hidden.
if (checkArguments()) {
final handledByUniLinks = await initUniLinks();
final handledByCli = checkArguments();
debugPrint(
"handled by uni links: $handledByUniLinks, handled by cli: $handledByCli");
if (handledByUniLinks || handledByCli) {
windowManager.hide();
} else {
windowManager.show();
@@ -139,8 +146,8 @@ void runMainApp(bool startService) async {
rustDeskWinManager.registerActiveWindow(kWindowMainId);
}
windowManager.setOpacity(1);
windowManager.setTitle(getWindowName());
});
windowManager.setTitle(getWindowName());
}
void runMobileApp() async {
@@ -208,7 +215,8 @@ void runMultiWindow(
}
void runConnectionManagerScreen(bool hide) async {
await initEnv(kAppTypeMain);
await initEnv(kAppTypeConnectionManager);
await bind.cmStartListenIpcThread();
_runApp(
'',
const DesktopServerPage(),
@@ -219,6 +227,8 @@ void runConnectionManagerScreen(bool hide) async {
} else {
showCmWindow();
}
// Start the uni links handler and redirect links to Native, not for Flutter.
_uniLinkSubscription = listenUniLinks(handleByFlutter: false);
}
void showCmWindow() {
@@ -350,6 +360,7 @@ class _AppState extends State<App> {
ChangeNotifierProvider.value(value: gFFI.imageModel),
ChangeNotifierProvider.value(value: gFFI.cursorModel),
ChangeNotifierProvider.value(value: gFFI.canvasModel),
ChangeNotifierProvider.value(value: gFFI.peerTabModel),
],
child: GetMaterialApp(
navigatorKey: globalKey,

View File

@@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/desktop/widgets/button.dart';
import 'package:get/get.dart';
import '../../common.dart';
@@ -371,8 +370,7 @@ void showWaitUacDialog(
tag: '$id-wait-uac',
(setState, close) => CustomAlertDialog(
title: null,
content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip')
.marginOnly(bottom: 10),
content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'),
));
}
@@ -645,10 +643,9 @@ class _PasswordWidgetState extends State<PasswordWidget> {
// Here is key idea
suffixIcon: IconButton(
icon: Icon(
// Based on passwordVisible state choose the icon
_passwordVisible ? Icons.visibility : Icons.visibility_off,
color: Theme.of(context).primaryColorDark,
),
// Based on passwordVisible state choose the icon
_passwordVisible ? Icons.visibility : Icons.visibility_off,
color: MyTheme.lightTheme.primaryColor),
onPressed: () {
// Update the state i.e. toggle the state of passwordVisible variable
setState(() {

View File

@@ -5,6 +5,7 @@ import 'package:draggable_float_widget/draggable_float_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
import '../consts.dart';
@@ -31,10 +32,14 @@ class ChatModel with ChangeNotifier {
OverlayEntry? chatIconOverlayEntry;
OverlayEntry? chatWindowOverlayEntry;
bool isConnManager = false;
RxBool isWindowFocus = true.obs;
BlockableOverlayState? _blockableOverlayState;
final Rx<VoiceCallStatus> _voiceCallStatus = Rx(VoiceCallStatus.notStarted);
Rx<VoiceCallStatus> get voiceCallStatus => _voiceCallStatus;
final ChatUser me = ChatUser(
id: "",
@@ -312,4 +317,34 @@ class ChatModel with ChangeNotifier {
}
});
}
void onVoiceCallWaiting() {
_voiceCallStatus.value = VoiceCallStatus.waitingForResponse;
}
void onVoiceCallStarted() {
_voiceCallStatus.value = VoiceCallStatus.connected;
}
void onVoiceCallClosed(String reason) {
_voiceCallStatus.value = VoiceCallStatus.notStarted;
}
void onVoiceCallIncoming() {
if (isConnManager) {
_voiceCallStatus.value = VoiceCallStatus.incoming;
}
}
void closeVoiceCall(String id) {
bind.sessionCloseVoiceCall(id: id);
}
}
enum VoiceCallStatus {
notStarted,
waitingForResponse,
connected,
// Connection manager only.
incoming
}

View File

@@ -35,7 +35,7 @@ class GroupModel {
await reset();
if (gFFI.userModel.userName.isEmpty ||
(gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
statePeerTab.check();
gFFI.peerTabModel.check_dynamic_tabs();
return;
}
userLoading.value = true;
@@ -82,7 +82,7 @@ class GroupModel {
userLoadError.value = err.toString();
} finally {
userLoading.value = false;
statePeerTab.check();
gFFI.peerTabModel.check_dynamic_tabs();
}
}

View File

@@ -310,7 +310,6 @@ class InputModel {
}
}
int _signOrZero(num x) {
if (x == 0) {
return 0;
@@ -362,7 +361,6 @@ class InputModel {
trackpadScrollDistance = Offset.zero;
}
void onPointDownImage(PointerDownEvent e) {
debugPrint("onPointDownImage");
if (e.kind != ui.PointerDeviceKind.mouse) {

View File

@@ -13,6 +13,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/group_model.dart';
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';
@@ -197,6 +198,26 @@ class FfiModel with ChangeNotifier {
final peer_id = evt['peer_id'].toString();
await bind.sessionSwitchSides(id: peer_id);
closeConnection(id: peer_id);
} else if (name == "on_url_scheme_received") {
final url = evt['url'].toString();
parseRustdeskUri(url);
} else if (name == "on_voice_call_waiting") {
// Waiting for the response from the peer.
parent.target?.chatModel.onVoiceCallWaiting();
} else if (name == "on_voice_call_started") {
// Voice call is connected.
parent.target?.chatModel.onVoiceCallStarted();
} else if (name == "on_voice_call_closed") {
// Voice call is closed with reason.
final reason = evt['reason'].toString();
parent.target?.chatModel.onVoiceCallClosed(reason);
} else if (name == "on_voice_call_incoming") {
// Voice call is requested by the peer.
parent.target?.chatModel.onVoiceCallIncoming();
} else if (name == "update_voice_call_state") {
parent.target?.serverModel.updateVoiceCallState(evt);
} else {
debugPrint("Unknown event name: $name");
}
};
}
@@ -242,7 +263,6 @@ class FfiModel with ChangeNotifier {
parent.target?.canvasModel.updateViewStyle();
}
parent.target?.recordingModel.onSwitchDisplay();
parent.target?.inputModel.refreshMousePos();
notifyListeners();
}
@@ -538,6 +558,7 @@ class CanvasModel with ChangeNotifier {
double _y = 0;
// image scale
double _scale = 1.0;
double _devicePixelRatio = 1.0;
Size _size = Size.zero;
// the tabbar over the image
// double tabBarHeight = 0.0;
@@ -561,6 +582,7 @@ class CanvasModel with ChangeNotifier {
double get x => _x;
double get y => _y;
double get scale => _scale;
double get devicePixelRatio => _devicePixelRatio;
Size get size => _size;
ScrollStyle get scrollStyle => _scrollStyle;
ViewStyle get viewStyle => _lastViewStyle;
@@ -609,13 +631,15 @@ class CanvasModel with ChangeNotifier {
_lastViewStyle = viewStyle;
_scale = viewStyle.scale;
_devicePixelRatio = ui.window.devicePixelRatio;
if (kIgnoreDpi && style == kRemoteViewStyleOriginal) {
_scale = 1.0 / ui.window.devicePixelRatio;
_scale = 1.0 / _devicePixelRatio;
}
_x = (size.width - displayWidth * _scale) / 2;
_y = (size.height - displayHeight * _scale) / 2;
_imageOverflow.value = _x < 0 || y < 0;
notifyListeners();
parent.target?.inputModel.refreshMousePos();
}
updateScrollStyle() async {
@@ -745,7 +769,7 @@ class CanvasModel with ChangeNotifier {
class CursorData {
final String peerId;
final int id;
final img2.Image? image;
final img2.Image image;
double scale;
Uint8List? data;
final double hotxOrigin;
@@ -770,33 +794,40 @@ class CursorData {
int _doubleToInt(double v) => (v * 10e6).round().toInt();
double _checkUpdateScale(double scale, bool shouldScale) {
double _checkUpdateScale(double scale) {
double oldScale = this.scale;
if (!shouldScale) {
scale = 1.0;
} else {
if (scale != 1.0) {
// Update data if scale changed.
if (Platform.isWindows) {
final tgtWidth = (width * scale).toInt();
final tgtHeight = (width * scale).toInt();
if (tgtWidth < kMinCursorSize || tgtHeight < kMinCursorSize) {
double sw = kMinCursorSize.toDouble() / width;
double sh = kMinCursorSize.toDouble() / height;
scale = sw < sh ? sh : sw;
}
final tgtWidth = (width * scale).toInt();
final tgtHeight = (width * scale).toInt();
if (tgtWidth < kMinCursorSize || tgtHeight < kMinCursorSize) {
double sw = kMinCursorSize.toDouble() / width;
double sh = kMinCursorSize.toDouble() / height;
scale = sw < sh ? sh : sw;
}
}
if (Platform.isWindows) {
if (_doubleToInt(oldScale) != _doubleToInt(scale)) {
if (_doubleToInt(oldScale) != _doubleToInt(scale)) {
if (Platform.isWindows) {
data = img2
.copyResize(
image!,
image,
width: (width * scale).toInt(),
height: (height * scale).toInt(),
interpolation: img2.Interpolation.average,
)
.getBytes(format: img2.Format.bgra);
} else {
data = Uint8List.fromList(
img2.encodePng(
img2.copyResize(
image,
width: (width * scale).toInt(),
height: (height * scale).toInt(),
interpolation: img2.Interpolation.average,
),
),
);
}
}
@@ -806,8 +837,8 @@ class CursorData {
return scale;
}
String updateGetKey(double scale, bool shouldScale) {
scale = _checkUpdateScale(scale, shouldScale);
String updateGetKey(double scale) {
scale = _checkUpdateScale(scale);
return '${peerId}_${id}_${_doubleToInt(width * scale)}_${_doubleToInt(height * scale)}';
}
}
@@ -865,7 +896,7 @@ class PredefinedCursor {
_cache = CursorData(
peerId: '',
id: id,
image: _image2?.clone(),
image: _image2!.clone(),
scale: scale,
data: data,
hotxOrigin:
@@ -892,9 +923,10 @@ class CursorModel with ChangeNotifier {
double _hoty = 0;
double _displayOriginX = 0;
double _displayOriginY = 0;
DateTime? _firstUpdateMouseTime;
bool gotMouseControl = true;
DateTime _lastPeerMouse = DateTime.now()
.subtract(Duration(milliseconds: 2 * kMouseControlTimeoutMSec));
.subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec));
String id = '';
WeakReference<FFI> parent;
@@ -913,6 +945,15 @@ class CursorModel with ChangeNotifier {
DateTime.now().difference(_lastPeerMouse).inMilliseconds <
kMouseControlTimeoutMSec;
bool isConnIn2Secs() {
if (_firstUpdateMouseTime == null) {
_firstUpdateMouseTime = DateTime.now();
return true;
} else {
return DateTime.now().difference(_firstUpdateMouseTime!).inSeconds < 2;
}
}
CursorModel(this.parent);
Set<String> get cachedKeys => _cacheKeys;
@@ -1065,9 +1106,9 @@ class CursorModel with ChangeNotifier {
Future<bool> _updateCache(
Uint8List rgba, ui.Image image, int id, int w, int h) async {
Uint8List? data;
img2.Image? imgOrigin;
img2.Image imgOrigin =
img2.Image.fromBytes(w, h, rgba, format: img2.Format.rgba);
if (Platform.isWindows) {
imgOrigin = img2.Image.fromBytes(w, h, rgba, format: img2.Format.rgba);
data = imgOrigin.getBytes(format: img2.Format.bgra);
} else {
ByteData? imgBytes =
@@ -1109,8 +1150,10 @@ class CursorModel with ChangeNotifier {
/// Update the cursor position.
updateCursorPosition(Map<String, dynamic> evt, String id) async {
gotMouseControl = false;
_lastPeerMouse = DateTime.now();
if (!isConnIn2Secs()) {
gotMouseControl = false;
_lastPeerMouse = DateTime.now();
}
_x = double.parse(evt['x']);
_y = double.parse(evt['y']);
try {
@@ -1265,8 +1308,9 @@ class FFI {
late final AbModel abModel; // global
late final GroupModel groupModel; // global
late final UserModel userModel; // global
late final PeerTabModel peerTabModel; // global
late final QualityMonitorModel qualityMonitorModel; // session
late final RecordingModel recordingModel; // recording
late final RecordingModel recordingModel; // session
late final InputModel inputModel; // session
FFI() {
@@ -1278,6 +1322,7 @@ class FFI {
chatModel = ChatModel(WeakReference(this));
fileModel = FileModel(WeakReference(this));
userModel = UserModel(WeakReference(this));
peerTabModel = PeerTabModel(WeakReference(this));
abModel = AbModel(WeakReference(this));
groupModel = GroupModel(WeakReference(this));
qualityMonitorModel = QualityMonitorModel(WeakReference(this));

View File

@@ -8,6 +8,7 @@ import 'package:external_path/external_path.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:win32/win32.dart' as win32;
@@ -46,6 +47,8 @@ class PlatformFFI {
static get localeName => Platform.localeName;
static get isMain => instance._appType == kAppTypeMain;
static Future<String> getVersion() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
return packageInfo.version;
@@ -112,8 +115,15 @@ class PlatformFFI {
}
_ffiBind = RustdeskImpl(dylib);
if (Platform.isLinux) {
// start dbus service, no need to await
await _ffiBind.mainStartDbusServer();
// Start a dbus service, no need to await
_ffiBind.mainStartDbusServer();
} else if (Platform.isMacOS && isMain) {
Future.wait([
// Start dbus service.
_ffiBind.mainStartDbusServer(),
// Start local audio pulseaudio server.
_ffiBind.mainStartPa()
]);
}
_startListenEvent(_ffiBind); // global event
try {

View File

@@ -0,0 +1,275 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:scroll_pos/scroll_pos.dart';
import '../common.dart';
import 'model.dart';
const int groupTabIndex = 4;
const String defaultGroupTabname = 'Group';
class PeerTabModel with ChangeNotifier {
WeakReference<FFI> parent;
int get currentTab => _currentTab;
int _currentTab = 0; // index in tabNames
List<int> get visibleOrderedTabs => _visibleOrderedTabs;
List<int> _visibleOrderedTabs = List.empty(growable: true);
List<int> get tabOrder => _tabOrder;
List<int> _tabOrder = List.from([0, 1, 2, 3, 4]); // constant length
int get tabHiddenFlag => _tabHiddenFlag;
int _tabHiddenFlag = 0;
bool get showScrollBtn => _showScrollBtn;
bool _showScrollBtn = false;
final List<bool> _fullyVisible = List.filled(5, false);
bool get leftFullyVisible => _leftFullyVisible;
bool _leftFullyVisible = false;
bool get rightFullyVisible => _rightFullyVisible;
bool _rightFullyVisible = false;
ScrollPosController sc = ScrollPosController();
List<String> tabNames = [
'Recent Sessions',
'Favorites',
'Discovered',
'Address Book',
defaultGroupTabname,
];
PeerTabModel(this.parent) {
// init tabHiddenFlag
_tabHiddenFlag = int.tryParse(
bind.getLocalFlutterConfig(k: 'hidden-peer-card'),
radix: 2) ??
0;
var tabs = _notHiddenTabs();
// remove dynamic tabs
tabs.remove(groupTabIndex);
// init tabOrder
try {
final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order');
if (conf.isNotEmpty) {
final json = jsonDecode(conf);
if (json is List) {
final List<int> list =
json.map((e) => int.tryParse(e.toString()) ?? -1).toList();
if (list.length == _tabOrder.length &&
_tabOrder.every((e) => list.contains(e))) {
_tabOrder = list;
}
}
}
} catch (e) {
debugPrintStack(label: '$e');
}
// init visibleOrderedTabs
var tempList = _tabOrder.toList();
tempList.removeWhere((e) => !tabs.contains(e));
_visibleOrderedTabs = tempList;
// init currentTab
_currentTab =
int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0;
if (!tabs.contains(_currentTab)) {
if (tabs.isNotEmpty) {
_currentTab = tabs[0];
} else {
_currentTab = 0;
}
}
sc.itemCount = _visibleOrderedTabs.length;
}
check_dynamic_tabs() {
var visible = visibleTabs();
_visibleOrderedTabs = _tabOrder.where((e) => visible.contains(e)).toList();
if (_visibleOrderedTabs.contains(groupTabIndex) &&
int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ==
groupTabIndex) {
_currentTab = groupTabIndex;
}
if (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isNotEmpty) {
tabNames[groupTabIndex] = gFFI.userModel.groupName.value;
} else {
tabNames[groupTabIndex] = defaultGroupTabname;
}
sc.itemCount = _visibleOrderedTabs.length;
notifyListeners();
}
setCurrentTab(int index) {
if (_currentTab != index) {
_currentTab = index;
notifyListeners();
}
}
setTabFullyVisible(int index, bool visible) {
if (index >= 0 && index < _fullyVisible.length) {
if (visible != _fullyVisible[index]) {
_fullyVisible[index] = visible;
bool changed = false;
bool show = _visibleOrderedTabs.any((e) => !_fullyVisible[e]);
if (show != _showScrollBtn) {
_showScrollBtn = show;
changed = true;
}
if (_visibleOrderedTabs.isNotEmpty && _visibleOrderedTabs[0] == index) {
if (_leftFullyVisible != visible) {
_leftFullyVisible = visible;
changed = true;
}
}
if (_visibleOrderedTabs.isNotEmpty &&
_visibleOrderedTabs.last == index) {
if (_rightFullyVisible != visible) {
_rightFullyVisible = visible;
changed = true;
}
}
if (changed) {
notifyListeners();
}
}
}
}
onReorder(oldIndex, newIndex) {
if (oldIndex < newIndex) {
newIndex -= 1;
}
var list = _visibleOrderedTabs.toList();
final int item = list.removeAt(oldIndex);
list.insert(newIndex, item);
_visibleOrderedTabs = list;
var tmpTabOrder = _visibleOrderedTabs.toList();
var left = _tabOrder.where((e) => !tmpTabOrder.contains(e)).toList();
for (var t in left) {
_addTabInOrder(tmpTabOrder, t);
}
_tabOrder = tmpTabOrder;
bind.setLocalFlutterConfig(k: 'peer-tab-order', v: jsonEncode(tmpTabOrder));
notifyListeners();
}
onHideShow(int index, bool show) async {
int bitMask = 1 << index;
if (show) {
_tabHiddenFlag &= ~bitMask;
} else {
_tabHiddenFlag |= bitMask;
}
await bind.setLocalFlutterConfig(
k: 'hidden-peer-card', v: _tabHiddenFlag.toRadixString(2));
var visible = visibleTabs();
_visibleOrderedTabs = _tabOrder.where((e) => visible.contains(e)).toList();
if (_visibleOrderedTabs.isNotEmpty &&
!_visibleOrderedTabs.contains(_currentTab)) {
_currentTab = _visibleOrderedTabs[0];
}
notifyListeners();
}
List<int> orderedNotFilteredTabs() {
var list = tabOrder.toList();
if (_filterGroupCard()) {
list.remove(groupTabIndex);
}
return list;
}
// return index array of tabNames
List<int> visibleTabs() {
var v = List<int>.empty(growable: true);
for (int i = 0; i < tabNames.length; i++) {
if (!_isTabHidden(i) && !_isTabFilter(i)) {
v.add(i);
}
}
return v;
}
String translatedTabname(int index) {
if (index >= 0 && index < tabNames.length) {
final name = tabNames[index];
if (index == groupTabIndex) {
if (name == defaultGroupTabname) {
return translate(name);
} else {
return name;
}
} else {
return translate(name);
}
}
assert(false);
return index.toString();
}
bool _isTabHidden(int tabindex) {
return _tabHiddenFlag & (1 << tabindex) != 0;
}
bool _isTabFilter(int tabIndex) {
if (tabIndex == groupTabIndex) {
return _filterGroupCard();
}
return false;
}
// return true if hide group card
bool _filterGroupCard() {
if (gFFI.groupModel.users.isEmpty ||
(gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
return true;
} else {
return false;
}
}
List<int> _notHiddenTabs() {
var v = List<int>.empty(growable: true);
for (int i = 0; i < tabNames.length; i++) {
if (!_isTabHidden(i)) {
v.add(i);
}
}
return v;
}
// add tabIndex to list
_addTabInOrder(List<int> list, int tabIndex) {
if (!_tabOrder.contains(tabIndex) || list.contains(tabIndex)) {
return;
}
bool sameOrder = true;
int lastIndex = -1;
for (int i = 0; i < list.length; i++) {
var index = _tabOrder.lastIndexOf(list[i]);
if (index > lastIndex) {
lastIndex = index;
continue;
} else {
sameOrder = false;
break;
}
}
if (sameOrder) {
var indexInTabOrder = _tabOrder.indexOf(tabIndex);
var left = List.empty(growable: true);
for (int i = 0; i < indexInTabOrder; i++) {
left.add(_tabOrder[i]);
}
int insertIndex = list.lastIndexWhere((e) => left.contains(e));
if (insertIndex < 0) {
insertIndex = 0;
} else {
insertIndex += 1;
}
list.insert(insertIndex, tabIndex);
} else {
list.add(tabIndex);
}
}
}

View File

@@ -579,6 +579,26 @@ class ServerModel with ChangeNotifier {
notifyListeners();
}
}
void updateVoiceCallState(Map<String, dynamic> evt) {
try {
final client = Client.fromJson(jsonDecode(evt["client"]));
final index = _clients.indexWhere((element) => element.id == client.id);
if (index != -1) {
_clients[index].inVoiceCall = client.inVoiceCall;
_clients[index].incomingVoiceCall = client.incomingVoiceCall;
if (client.incomingVoiceCall) {
// Has incoming phone call, let's set the window on top.
Future.delayed(Duration.zero, () {
window_on_top(null);
});
}
notifyListeners();
}
} catch (e) {
debugPrint("updateVoiceCallState failed: $e");
}
}
}
enum ClientType {
@@ -602,6 +622,8 @@ class Client {
bool recording = false;
bool disconnected = false;
bool fromSwitch = false;
bool inVoiceCall = false;
bool incomingVoiceCall = false;
RxBool hasUnreadChatMessage = false.obs;
@@ -623,6 +645,8 @@ class Client {
recording = json['recording'];
disconnected = json['disconnected'];
fromSwitch = json['from_switch'];
inVoiceCall = json['in_voice_call'];
incomingVoiceCall = json['incoming_voice_call'];
}
Map<String, dynamic> toJson() {

View File

@@ -62,7 +62,7 @@ class UserModel {
await gFFI.groupModel.reset();
userName.value = '';
groupName.value = '';
statePeerTab.check();
gFFI.peerTabModel.check_dynamic_tabs();
}
Future<void> _parseAndUpdateUser(UserPayload user) async {

View File

@@ -160,6 +160,24 @@ class RustDeskMultiWindowManager {
return null;
}
void clearWindowType(WindowType type) {
switch (type) {
case WindowType.Main:
return;
case WindowType.RemoteDesktop:
_remoteDesktopWindowId = null;
break;
case WindowType.FileTransfer:
_fileTransferWindowId = null;
break;
case WindowType.PortForward:
_portForwardWindowId = null;
break;
case WindowType.Unknown:
break;
}
}
void setMethodHandler(
Future<dynamic> Function(MethodCall call, int fromWindowId)? handler) {
DesktopMultiWindow.setMethodHandler(handler);
@@ -186,8 +204,11 @@ class RustDeskMultiWindowManager {
}
await WindowController.fromWindowId(wId).setPreventClose(false);
await WindowController.fromWindowId(wId).close();
} on Error {
} catch (e) {
debugPrint("$e");
return;
} finally {
clearWindowType(type);
}
}
}

View File

@@ -31,4 +31,10 @@ class RdPlatformChannel {
return _osxMethodChannel
.invokeMethod("setWindowTheme", {"themeName": theme.name});
}
/// Terminate .app manually.
Future<void> terminate() {
assert(Platform.isMacOS);
return _osxMethodChannel.invokeMethod("terminate");
}
}