Merge pull request #1693 from fufesou/flutter_remote_adjust_window

Flutter remote adjust window
This commit is contained in:
RustDesk 2022-10-10 11:14:18 +08:00 committed by GitHub
commit b5809f1315
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 523 additions and 233 deletions

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui';
import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:back_button_interceptor/back_button_interceptor.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
@ -15,6 +16,7 @@ import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:window_size/window_size.dart' as window_size;
import 'common/widgets/overlay.dart'; import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart'; import 'mobile/pages/file_manager_page.dart';
@ -23,6 +25,8 @@ import 'models/input_model.dart';
import 'models/model.dart'; import 'models/model.dart';
import 'models/platform_model.dart'; import 'models/platform_model.dart';
import '../consts.dart';
final globalKey = GlobalKey<NavigatorState>(); final globalKey = GlobalKey<NavigatorState>();
final navigationBarKey = GlobalKey(); final navigationBarKey = GlobalKey();
@ -1022,6 +1026,89 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
} }
} }
_adjustRestoreMainWindowSize(double? width, double? height) async {
const double minWidth = 600;
const double minHeight = 100;
double maxWidth = (((isDesktop || isWebDesktop)
? kDesktopMaxDisplayWidth
: kMobileMaxDisplayWidth))
.toDouble();
double maxHeight = ((isDesktop || isWebDesktop)
? kDesktopMaxDisplayHeight
: kMobileMaxDisplayHeight)
.toDouble();
if (isDesktop || isWebDesktop) {
final screen = (await window_size.getWindowInfo()).screen;
if (screen != null) {
maxWidth = screen.visibleFrame.width;
maxHeight = screen.visibleFrame.height;
}
}
final defaultWidth =
((isDesktop || isWebDesktop) ? 1280 : kMobileDefaultDisplayWidth)
.toDouble();
final defaultHeight =
((isDesktop || isWebDesktop) ? 720 : kMobileDefaultDisplayHeight)
.toDouble();
double restoreWidth = width ?? defaultWidth;
double restoreHeight = height ?? defaultHeight;
if (restoreWidth < minWidth) {
restoreWidth = minWidth;
}
if (restoreHeight < minHeight) {
restoreHeight = minHeight;
}
if (restoreWidth > maxWidth) {
restoreWidth = maxWidth;
}
if (restoreHeight > maxHeight) {
restoreWidth = maxHeight;
}
await windowManager.setSize(Size(restoreWidth, restoreHeight));
}
_adjustRestoreMainWindowOffset(double? left, double? top) async {
if (left == null || top == null) {
await windowManager.center();
} else {
double windowLeft = left;
double windowTop = top;
double frameLeft = 0;
double frameTop = 0;
double frameRight = ((isDesktop || isWebDesktop)
? kDesktopMaxDisplayWidth
: kMobileMaxDisplayWidth)
.toDouble();
double frameBottom = ((isDesktop || isWebDesktop)
? kDesktopMaxDisplayHeight
: kMobileMaxDisplayHeight)
.toDouble();
if (isDesktop || isWebDesktop) {
final screen = (await window_size.getWindowInfo()).screen;
if (screen != null) {
frameLeft = screen.visibleFrame.left;
frameTop = screen.visibleFrame.top;
frameRight = screen.visibleFrame.right;
frameBottom = screen.visibleFrame.bottom;
}
}
if (windowLeft < frameLeft ||
windowLeft > frameRight ||
windowTop < frameTop ||
windowTop > frameBottom) {
await windowManager.center();
} else {
await windowManager.setPosition(Offset(windowLeft, windowTop));
}
}
}
/// Save window position and size on exit /// Save window position and size on exit
/// Note that windowId must be provided if it's subwindow /// Note that windowId must be provided if it's subwindow
Future<bool> restoreWindowPosition(WindowType type, {int? windowId}) async { Future<bool> restoreWindowPosition(WindowType type, {int? windowId}) async {
@ -1042,13 +1129,10 @@ Future<bool> restoreWindowPosition(WindowType type, {int? windowId}) async {
debugPrint("window position saved, but cannot be parsed"); debugPrint("window position saved, but cannot be parsed");
return false; return false;
} }
await windowManager.setSize(Size(lpos.width ?? 1280, lpos.height ?? 720));
if (lpos.offsetWidth == null || lpos.offsetHeight == null) { await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
await windowManager.center(); await _adjustRestoreMainWindowOffset(lpos.offsetWidth, lpos.offsetHeight);
} else {
await windowManager
.setPosition(Offset(lpos.offsetWidth!, lpos.offsetHeight!));
}
return true; return true;
default: default:
// TODO: implement subwindow // TODO: implement subwindow

View File

@ -179,3 +179,25 @@ class RemoteCursorMovedState {
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id)); static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
} }
class RemoteCountState {
static String tag() => 'remote_count_';
static void init() {
final key = tag();
if (!Get.isRegistered(tag: key)) {
// Server side, default true
final RxInt state = 1.obs;
Get.put(state, tag: key);
}
}
static void delete() {
final key = tag();
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
}
}
static RxInt find() => Get.find<RxInt>(tag: tag());
}

View File

@ -20,6 +20,12 @@ const int kMobileDefaultDisplayHeight = 1280;
const int kDesktopDefaultDisplayWidth = 1080; const int kDesktopDefaultDisplayWidth = 1080;
const int kDesktopDefaultDisplayHeight = 720; const int kDesktopDefaultDisplayHeight = 720;
const int kMobileMaxDisplayWidth = 720;
const int kMobileMaxDisplayHeight = 1280;
const int kDesktopMaxDisplayWidth = 1920;
const int kDesktopMaxDisplayHeight = 1080;
const Size kConnectionManagerWindowSize = Size(300, 400); const Size kConnectionManagerWindowSize = Size(300, 400);
// Tabbar transition duration, now we remove the duration // Tabbar transition duration, now we remove the duration
const Duration kTabTransitionDuration = Duration.zero; const Duration kTabTransitionDuration = Duration.zero;

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:convert';
import 'package:flutter/material.dart' hide MenuItem; import 'package:flutter/material.dart' hide MenuItem;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -16,6 +17,7 @@ import 'package:provider/provider.dart';
import 'package:tray_manager/tray_manager.dart'; import 'package:tray_manager/tray_manager.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:window_size/window_size.dart' as window_size;
import '../widgets/button.dart'; import '../widgets/button.dart';
@ -427,6 +429,27 @@ class _DesktopHomePageState extends State<DesktopHomePage>
"call ${call.method} with args ${call.arguments} from window $fromWindowId"); "call ${call.method} with args ${call.arguments} from window $fromWindowId");
if (call.method == "main_window_on_top") { if (call.method == "main_window_on_top") {
window_on_top(null); window_on_top(null);
} else if (call.method == "get_window_info") {
final screen = (await window_size.getWindowInfo()).screen;
if (screen == null) {
return "";
} else {
return jsonEncode({
'frame': {
'l': screen.frame.left,
't': screen.frame.top,
'r': screen.frame.right,
'b': screen.frame.bottom,
},
'visibleFrame': {
'l': screen.visibleFrame.left,
't': screen.visibleFrame.top,
'r': screen.visibleFrame.right,
'b': screen.visibleFrame.bottom,
},
'scaleFactor': screen.scaleFactor,
});
}
} }
}); });
} }

View File

@ -9,6 +9,8 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import '../../common/shared_state.dart';
class DesktopTabPage extends StatefulWidget { class DesktopTabPage extends StatefulWidget {
const DesktopTabPage({Key? key}) : super(key: key); const DesktopTabPage({Key? key}) : super(key: key);
@ -40,6 +42,7 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
void initState() { void initState() {
super.initState(); super.initState();
Get.put<DesktopTabController>(tabController); Get.put<DesktopTabController>(tabController);
RemoteCountState.init();
tabController.add(TabInfo( tabController.add(TabInfo(
key: kTabLabelHomePage, key: kTabLabelHomePage,
label: kTabLabelHomePage, label: kTabLabelHomePage,

View File

@ -28,11 +28,13 @@ class RemotePage extends StatefulWidget {
const RemotePage({ const RemotePage({
Key? key, Key? key,
required this.id, required this.id,
required this.windowId,
required this.tabBarHeight, required this.tabBarHeight,
required this.windowBorderWidth, required this.windowBorderWidth,
}) : super(key: key); }) : super(key: key);
final String id; final String id;
final int windowId;
final double tabBarHeight; final double tabBarHeight;
final double windowBorderWidth; final double windowBorderWidth;
@ -239,6 +241,7 @@ class _RemotePageState extends State<RemotePage>
paints.add(QualityMonitor(_ffi.qualityMonitorModel)); paints.add(QualityMonitor(_ffi.qualityMonitorModel));
paints.add(RemoteMenubar( paints.add(RemoteMenubar(
id: widget.id, id: widget.id,
windowId: widget.windowId,
ffi: _ffi, ffi: _ffi,
onEnterOrLeaveImageSetter: (func) => _onEnterOrLeaveImage4Menubar = func, onEnterOrLeaveImageSetter: (func) => _onEnterOrLeaveImage4Menubar = func,
onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Menubar = null, onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Menubar = null,

View File

@ -32,6 +32,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
var connectionMap = RxList<Widget>.empty(growable: true); var connectionMap = RxList<Widget>.empty(growable: true);
_ConnectionTabPageState(Map<String, dynamic> params) { _ConnectionTabPageState(Map<String, dynamic> params) {
RemoteCountState.init();
final RxBool fullscreen = Get.find(tag: 'fullscreen'); final RxBool fullscreen = Get.find(tag: 'fullscreen');
final peerId = params['id']; final peerId = params['id'];
if (peerId != null) { if (peerId != null) {
@ -45,10 +46,12 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
page: Obx(() => RemotePage( page: Obx(() => RemotePage(
key: ValueKey(peerId), key: ValueKey(peerId),
id: peerId, id: peerId,
windowId: windowId(),
tabBarHeight: tabBarHeight:
fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight,
windowBorderWidth: fullscreen.isTrue ? 0 : kWindowBorderWidth, windowBorderWidth: fullscreen.isTrue ? 0 : kWindowBorderWidth,
)))); ))));
_update_remote_count();
} }
} }
@ -79,6 +82,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
page: Obx(() => RemotePage( page: Obx(() => RemotePage(
key: ValueKey(id), key: ValueKey(id),
id: id, id: id,
windowId: windowId(),
tabBarHeight: tabBarHeight:
fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight,
windowBorderWidth: fullscreen.isTrue ? 0 : kWindowBorderWidth, windowBorderWidth: fullscreen.isTrue ? 0 : kWindowBorderWidth,
@ -86,6 +90,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
} else if (call.method == "onDestroy") { } else if (call.method == "onDestroy") {
tabController.clear(); tabController.clear();
} }
_update_remote_count();
}); });
} }
@ -161,6 +166,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
WindowController.fromWindowId(windowId()).hide(); WindowController.fromWindowId(windowId()).hide();
} }
ConnectionTypeState.delete(id); ConnectionTypeState.delete(id);
_update_remote_count();
} }
int windowId() { int windowId() {
@ -178,7 +184,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
} }
Future<bool> handleWindowCloseButton() async { Future<bool> handleWindowCloseButton() async {
final connLength = tabController.state.value.tabs.length; final connLength = tabController.length;
if (connLength < 1) { if (connLength < 1) {
return true; return true;
} else if (connLength == 1) { } else if (connLength == 1) {
@ -189,8 +195,12 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final res = await closeConfirmDialog(); final res = await closeConfirmDialog();
if (res) { if (res) {
tabController.clear(); tabController.clear();
_update_remote_count();
} }
return res; return res;
} }
} }
_update_remote_count() =>
RemoteCountState.find().value = tabController.length;
} }

View File

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -8,6 +9,8 @@ import 'package:flutter_hbb/models/chat_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:rxdart/rxdart.dart' as rxdart; import 'package:rxdart/rxdart.dart' as rxdart;
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:window_size/window_size.dart' as window_size;
import '../../common.dart'; import '../../common.dart';
import '../../mobile/widgets/dialog.dart'; import '../../mobile/widgets/dialog.dart';
@ -26,6 +29,7 @@ class _MenubarTheme {
class RemoteMenubar extends StatefulWidget { class RemoteMenubar extends StatefulWidget {
final String id; final String id;
final int windowId;
final FFI ffi; final FFI ffi;
final Function(Function(bool)) onEnterOrLeaveImageSetter; final Function(Function(bool)) onEnterOrLeaveImageSetter;
final Function() onEnterOrLeaveImageCleaner; final Function() onEnterOrLeaveImageCleaner;
@ -33,6 +37,7 @@ class RemoteMenubar extends StatefulWidget {
const RemoteMenubar({ const RemoteMenubar({
Key? key, Key? key,
required this.id, required this.id,
required this.windowId,
required this.ffi, required this.ffi,
required this.onEnterOrLeaveImageSetter, required this.onEnterOrLeaveImageSetter,
required this.onEnterOrLeaveImageCleaner, required this.onEnterOrLeaveImageCleaner,
@ -48,6 +53,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
final _rxHideReplay = rxdart.ReplaySubject<int>(); final _rxHideReplay = rxdart.ReplaySubject<int>();
final _pinMenubar = false.obs; final _pinMenubar = false.obs;
bool _isCursorOverImage = false; bool _isCursorOverImage = false;
window_size.Screen? _screen;
bool get isFullscreen => Get.find<RxBool>(tag: 'fullscreen').isTrue; bool get isFullscreen => Get.find<RxBool>(tag: 'fullscreen').isTrue;
void _setFullscreen(bool v) { void _setFullscreen(bool v) {
@ -105,12 +111,34 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}, },
onPressed: () { onPressed: () {
_show.value = !_show.value; _show.value = !_show.value;
if (_show.isTrue) {
_updateScreen();
}
}, },
child: Obx(() => Container( child: Obx(() => Container(
color: _hideColor.value, color: _hideColor.value,
).marginOnly(bottom: 8.0)))))); ).marginOnly(bottom: 8.0))))));
} }
_updateScreen() async {
final v = await DesktopMultiWindow.invokeMethod(0, "get_window_info", "");
final String valueStr = v;
if (valueStr.isEmpty) {
_screen = null;
} else {
final screenMap = jsonDecode(valueStr);
_screen = window_size.Screen(
Rect.fromLTRB(screenMap['frame']['l'], screenMap['frame']['t'],
screenMap['frame']['r'], screenMap['frame']['b']),
Rect.fromLTRB(
screenMap['visibleFrame']['l'],
screenMap['visibleFrame']['t'],
screenMap['visibleFrame']['r'],
screenMap['visibleFrame']['b']),
screenMap['scaleFactor']);
}
}
Widget _buildMenubar(BuildContext context) { Widget _buildMenubar(BuildContext context) {
final List<Widget> menubarItems = []; final List<Widget> menubarItems = [];
if (!isWebDesktop) { if (!isWebDesktop) {
@ -306,25 +334,29 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
return {'supportedHwcodec': supportedHwcodec}; return {'supportedHwcodec': supportedHwcodec};
}(), builder: (context, snapshot) { }(), builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
return mod_menu.PopupMenuButton( return Obx(() {
padding: EdgeInsets.zero, final remoteCount = RemoteCountState.find().value;
icon: const Icon( return mod_menu.PopupMenuButton(
Icons.tv, padding: EdgeInsets.zero,
color: _MenubarTheme.commonColor, icon: const Icon(
), Icons.tv,
tooltip: translate('Display Settings'), color: _MenubarTheme.commonColor,
position: mod_menu.PopupMenuPosition.under, ),
itemBuilder: (BuildContext context) => _getDisplayMenu(snapshot.data!) tooltip: translate('Display Settings'),
.map((entry) => entry.build( position: mod_menu.PopupMenuPosition.under,
context, itemBuilder: (BuildContext context) =>
const MenuConfig( _getDisplayMenu(snapshot.data!, remoteCount)
commonColor: _MenubarTheme.commonColor, .map((entry) => entry.build(
height: _MenubarTheme.height, context,
dividerHeight: _MenubarTheme.dividerHeight, const MenuConfig(
))) commonColor: _MenubarTheme.commonColor,
.expand((i) => i) height: _MenubarTheme.height,
.toList(), dividerHeight: _MenubarTheme.dividerHeight,
); )))
.expand((i) => i)
.toList(),
);
});
} else { } else {
return const Offstage(); return const Offstage();
} }
@ -586,7 +618,34 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
return displayMenu; return displayMenu;
} }
List<MenuEntryBase<String>> _getDisplayMenu(dynamic futureData) { bool _isWindowCanBeAdjusted(int remoteCount) {
if (remoteCount != 1) {
return false;
}
if (_screen == null) {
return false;
}
double scale = _screen!.scaleFactor;
double selfWidth = _screen!.frame.width;
double selfHeight = _screen!.frame.height;
if (isFullscreen) {
selfWidth = _screen!.visibleFrame.width;
selfHeight = _screen!.visibleFrame.height;
}
final canvasModel = widget.ffi.canvasModel;
final displayWidth = canvasModel.getDisplayWidth();
final displayHeight = canvasModel.getDisplayHeight();
final requiredWidth = displayWidth +
(canvasModel.tabBarHeight + canvasModel.windowBorderWidth * 2);
final requiredHeight = displayHeight +
(canvasModel.tabBarHeight + canvasModel.windowBorderWidth * 2);
return selfWidth > (requiredWidth * scale) &&
selfHeight > (requiredHeight * scale);
}
List<MenuEntryBase<String>> _getDisplayMenu(
dynamic futureData, int remoteCount) {
const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0); const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0);
final displayMenu = [ final displayMenu = [
MenuEntryRadios<String>( MenuEntryRadios<String>(
@ -739,6 +798,77 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
MenuEntryDivider<String>(), MenuEntryDivider<String>(),
]; ];
if (_isWindowCanBeAdjusted(remoteCount)) {
displayMenu.insert(
0,
MenuEntryDivider<String>(),
);
displayMenu.insert(
0,
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Container(
child: Text(
translate('Adjust Window'),
style: style,
)),
proc: () {
() async {
await _updateScreen();
if (_screen != null) {
_setFullscreen(false);
double scale = _screen!.scaleFactor;
final wndRect =
await WindowController.fromWindowId(widget.windowId)
.getFrame();
final mediaSize = MediaQueryData.fromWindow(ui.window).size;
// On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect.
// https://stackoverflow.com/a/7561083
double magicWidth =
wndRect.right - wndRect.left - mediaSize.width * scale;
double magicHeight =
wndRect.bottom - wndRect.top - mediaSize.height * scale;
final canvasModel = widget.ffi.canvasModel;
final width = (canvasModel.getDisplayWidth() +
canvasModel.windowBorderWidth * 2) *
scale +
magicWidth;
final height = (canvasModel.getDisplayHeight() +
canvasModel.tabBarHeight +
canvasModel.windowBorderWidth * 2) *
scale +
magicHeight;
double left = wndRect.left + (wndRect.width - width) / 2;
double top = wndRect.top + (wndRect.height - height) / 2;
Rect frameRect = _screen!.frame;
final RxBool fullscreen = Get.find(tag: 'fullscreen');
if (fullscreen.isFalse) {
frameRect = _screen!.visibleFrame;
}
if (left < frameRect.left) {
left = frameRect.left;
}
if (top < frameRect.top) {
top = frameRect.top;
}
if ((left + width) > frameRect.right) {
left = frameRect.right - width;
}
if ((top + height) > frameRect.bottom) {
top = frameRect.bottom - height;
}
await WindowController.fromWindowId(widget.windowId)
.setFrame(Rect.fromLTWH(left, top, width, height));
}
}();
},
padding: padding,
dismissOnClicked: true,
),
);
}
/// Show Codec Preference /// Show Codec Preference
if (bind.mainHasHwcodec()) { if (bind.mainHasHwcodec()) {
final List<bool> codecs = []; final List<bool> codecs = [];

View File

@ -69,6 +69,8 @@ class DesktopTabController {
DesktopTabController({required this.tabType}); DesktopTabController({required this.tabType});
int get length => state.value.tabs.length;
void add(TabInfo tab, {bool authorized = false}) { void add(TabInfo tab, {bool authorized = false}) {
if (!isDesktop) return; if (!isDesktop) return;
final index = state.value.tabs.indexWhere((e) => e.key == tab.key); final index = state.value.tabs.indexWhere((e) => e.key == tab.key);

File diff suppressed because it is too large Load Diff

View File

@ -73,7 +73,12 @@ dependencies:
flutter_custom_cursor: flutter_custom_cursor:
git: git:
url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor
ref: 4a950fd3a5a228bf5381070a4c803919d5787c07 ref: 4a950fd3a5a228bf5381070a4c803919d5787c07
window_size:
git:
url: https://github.com/google/flutter-desktop-embedding.git
path: plugins/window_size
ref: a738913c8ce2c9f47515382d40827e794a334274
get: ^4.6.5 get: ^4.6.5
visibility_detector: ^0.3.3 visibility_detector: ^0.3.3
contextmenu: ^3.0.0 contextmenu: ^3.0.0