flutter_desktop: new remote menu, mid commit

Signed-off-by: fufesou <shuanglongchen@yeah.net>
This commit is contained in:
fufesou
2022-08-26 23:28:08 +08:00
parent 7b4a655eaf
commit ea77d9284b
30 changed files with 2606 additions and 84 deletions

View File

@@ -447,7 +447,10 @@ void msgBox(
0,
wrap(translate('OK'), () {
dialogManager.dismissAll();
closeConnection();
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
if (type.indexOf("custom") < 0) {
closeConnection();
}
}));
}
if (hasCancel == null) {
@@ -740,3 +743,39 @@ Future<List<Peer>>? matchPeers(String searchText, List<Peer> peers) async {
}
return filteredList;
}
class PrivacyModeState {
static String tag(String id) => 'privacy_mode_' + id;
static void init(String id) {
final RxBool state = false.obs;
Get.put(state, tag: tag(id));
}
static void delete(String id) => Get.delete(tag: tag(id));
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class BlockInputState {
static String tag(String id) => 'block_input_' + id;
static void init(String id) {
final RxBool state = false.obs;
Get.put(state, tag: tag(id));
}
static void delete(String id) => Get.delete(tag: tag(id));
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class CurrentDisplayState {
static String tag(String id) => 'current_display_' + id;
static void init(String id) {
final RxInt state = RxInt(0);
Get.put(state, tag: tag(id));
}
static void delete(String id) => Get.delete(tag: tag(id));
static RxInt find(String id) => Get.find<RxInt>(tag: tag(id));
}

View File

@@ -22,26 +22,25 @@ class ConnectionTabPage extends StatefulWidget {
class _ConnectionTabPageState extends State<ConnectionTabPage> {
final tabController = Get.put(DesktopTabController());
static final Rx<String> _fullscreenID = "".obs;
static final IconData selectedIcon = Icons.desktop_windows_sharp;
static final IconData unselectedIcon = Icons.desktop_windows_outlined;
var connectionMap = RxList<Widget>.empty(growable: true);
_ConnectionTabPageState(Map<String, dynamic> params) {
final RxBool fullscreen = Get.find(tag: 'fullscreen');
if (params['id'] != null) {
tabController.add(TabInfo(
key: params['id'],
label: params['id'],
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
page: RemotePage(
key: ValueKey(params['id']),
id: params['id'],
tabBarHeight:
_fullscreenID.value.isNotEmpty ? 0 : kDesktopRemoteTabBarHeight,
fullscreenID: _fullscreenID,
)));
page: Obx(() => RemotePage(
key: ValueKey(params['id']),
id: params['id'],
tabBarHeight:
fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight,
))));
}
}
@@ -54,6 +53,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
print(
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
final RxBool fullscreen = Get.find(tag: 'fullscreen');
// for simplify, just replace connectionId
if (call.method == "new_remote_desktop") {
final args = jsonDecode(call.arguments);
@@ -64,14 +65,13 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
page: RemotePage(
key: ValueKey(id),
id: id,
tabBarHeight: _fullscreenID.value.isNotEmpty
? 0
: kDesktopRemoteTabBarHeight,
fullscreenID: _fullscreenID,
)));
closable: false,
page: Obx(() => RemotePage(
key: ValueKey(id),
id: id,
tabBarHeight:
fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight,
))));
} else if (call.method == "onDestroy") {
tabController.state.value.tabs.forEach((tab) {
print("executing onDestroy hook, closing ${tab.label}}");
@@ -88,29 +88,31 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
@override
Widget build(BuildContext context) {
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
return SubWindowDragToResizeArea(
windowId: windowId(),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: MyTheme.color(context).bg,
body: Obx(() => DesktopTab(
controller: tabController,
theme: theme,
isMainWindow: false,
showTabBar: _fullscreenID.value.isEmpty,
tail: AddButton(
theme: theme,
).paddingOnly(left: 10),
pageViewBuilder: (pageView) {
WindowController.fromWindowId(windowId())
.setFullscreen(_fullscreenID.value.isNotEmpty);
return pageView;
},
))),
),
);
final RxBool fullscreen = Get.find(tag: 'fullscreen');
return Obx(() => SubWindowDragToResizeArea(
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
windowId: windowId(),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: MyTheme.color(context).bg,
body: Obx(() => DesktopTab(
controller: tabController,
theme: theme,
isMainWindow: false,
showTabBar: fullscreen.isFalse,
tail: AddButton(
theme: theme,
).paddingOnly(left: 10),
pageViewBuilder: (pageView) {
WindowController.fromWindowId(windowId())
.setFullscreen(fullscreen.isTrue);
return pageView;
},
))),
),
));
}
void onRemoveId(String id) {

View File

@@ -4,6 +4,7 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
class DesktopTabPage extends StatefulWidget {
@@ -33,26 +34,29 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
@override
Widget build(BuildContext context) {
final dark = isDarkTheme();
return DragToResizeArea(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: MyTheme.color(context).bg,
body: DesktopTab(
controller: tabController,
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
isMainWindow: true,
tail: ActionIcon(
message: 'Settings',
icon: IconFont.menu,
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
onTap: onAddSetting,
is_close: false,
),
)),
),
);
RxBool fullscreen = false.obs;
Get.put(fullscreen, tag: 'fullscreen');
return Obx(() => DragToResizeArea(
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: MyTheme.color(context).bg,
body: DesktopTab(
controller: tabController,
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
isMainWindow: true,
tail: ActionIcon(
message: 'Settings',
icon: IconFont.menu,
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
onTap: onAddSetting,
is_close: false,
),
)),
),
));
}
void onAddSetting() {

View File

@@ -9,9 +9,11 @@ import 'package:flutter_hbb/models/chat_model.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock/wakelock.dart';
import 'package:tuple/tuple.dart';
// import 'package:window_manager/window_manager.dart';
import '../widgets/remote_menubar.dart';
import '../../common.dart';
import '../../mobile/widgets/dialog.dart';
import '../../mobile/widgets/overlay.dart';
@@ -21,16 +23,14 @@ import '../../models/platform_model.dart';
final initText = '\1' * 1024;
class RemotePage extends StatefulWidget {
RemotePage(
{Key? key,
required this.id,
required this.tabBarHeight,
required this.fullscreenID})
: super(key: key);
RemotePage({
Key? key,
required this.id,
required this.tabBarHeight,
}) : super(key: key);
final String id;
final double tabBarHeight;
final Rx<String> fullscreenID;
@override
_RemotePageState createState() => _RemotePageState();
@@ -50,11 +50,15 @@ class _RemotePageState extends State<RemotePage>
late FFI _ffi;
void _updateTabBarHeight() {
_ffi.canvasModel.tabBarHeight = widget.tabBarHeight;
}
@override
void initState() {
super.initState();
_ffi = FFI();
_ffi.canvasModel.tabBarHeight = super.widget.tabBarHeight;
_updateTabBarHeight();
Get.put(_ffi, tag: widget.id);
_ffi.connect(widget.id, tabBarHeight: super.widget.tabBarHeight);
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -70,6 +74,9 @@ class _RemotePageState extends State<RemotePage>
_ffi.listenToMouse(true);
_ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
// WindowManager.instance.addListener(this);
PrivacyModeState.init(widget.id);
BlockInputState.init(widget.id);
CurrentDisplayState.init(widget.id);
}
@override
@@ -90,6 +97,9 @@ class _RemotePageState extends State<RemotePage>
// WindowManager.instance.removeListener(this);
Get.delete<FFI>(tag: widget.id);
super.dispose();
PrivacyModeState.delete(widget.id);
BlockInputState.delete(widget.id);
CurrentDisplayState.delete(widget.id);
}
void resetTool() {
@@ -217,6 +227,7 @@ class _RemotePageState extends State<RemotePage>
@override
Widget build(BuildContext context) {
super.build(context);
_updateTabBarHeight();
return WillPopScope(
onWillPop: () async {
clientClose(_ffi.dialogManager);
@@ -289,6 +300,7 @@ class _RemotePageState extends State<RemotePage>
}
Widget? getBottomAppBar(FfiModel ffiModel) {
final RxBool fullscreen = Get.find(tag: 'fullscreen');
return MouseRegion(
cursor: SystemMouseCursors.basic,
child: BottomAppBar(
@@ -323,15 +335,11 @@ class _RemotePageState extends State<RemotePage>
: <Widget>[
IconButton(
color: Colors.white,
icon: Icon(widget.fullscreenID.value.isEmpty
icon: Icon(fullscreen.isTrue
? Icons.fullscreen
: Icons.close_fullscreen),
onPressed: () {
if (widget.fullscreenID.value.isEmpty) {
widget.fullscreenID.value = widget.id;
} else {
widget.fullscreenID.value = "";
}
fullscreen.value = !fullscreen.value;
},
)
]) +
@@ -404,7 +412,7 @@ class _RemotePageState extends State<RemotePage>
}
if (_isPhysicalMouse) {
_ffi.handleMouse(getEvent(e, 'mousemove'),
tabBarHeight: super.widget.tabBarHeight);
tabBarHeight: widget.tabBarHeight);
}
}
@@ -418,7 +426,7 @@ class _RemotePageState extends State<RemotePage>
}
if (_isPhysicalMouse) {
_ffi.handleMouse(getEvent(e, 'mousedown'),
tabBarHeight: super.widget.tabBarHeight);
tabBarHeight: widget.tabBarHeight);
}
}
@@ -426,7 +434,7 @@ class _RemotePageState extends State<RemotePage>
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (_isPhysicalMouse) {
_ffi.handleMouse(getEvent(e, 'mouseup'),
tabBarHeight: super.widget.tabBarHeight);
tabBarHeight: widget.tabBarHeight);
}
}
@@ -434,7 +442,7 @@ class _RemotePageState extends State<RemotePage>
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (_isPhysicalMouse) {
_ffi.handleMouse(getEvent(e, 'mousemove'),
tabBarHeight: super.widget.tabBarHeight);
tabBarHeight: widget.tabBarHeight);
}
}
@@ -500,6 +508,10 @@ class _RemotePageState extends State<RemotePage>
));
}
paints.add(QualityMonitor(_ffi.qualityMonitorModel));
paints.add(RemoteMenubar(
id: widget.id,
ffi: _ffi,
));
return Stack(
children: paints,
);

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/connection_tab_page.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
/// multi-tab desktop remote screen
@@ -11,6 +12,8 @@ class DesktopRemoteScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
RxBool fullscreen = false.obs;
Get.put(fullscreen, tag: 'fullscreen');
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: gFFI.ffiModel),

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,375 @@
import 'dart:core';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tuple/tuple.dart';
import './material_mod_popup_menu.dart' as modMenu;
const kInvalidValueStr = "InvalidValueStr";
// https://stackoverflow.com/questions/68318314/flutter-popup-menu-inside-popup-menu
class PopupMenuChildrenItem<T> extends modMenu.PopupMenuEntry<T> {
const PopupMenuChildrenItem({
key,
this.height = kMinInteractiveDimension,
this.padding,
this.enable = true,
this.textStyle,
this.onTap,
this.position = modMenu.PopupMenuPosition.overSide,
this.offset = Offset.zero,
required this.itemBuilder,
required this.child,
}) : super(key: key);
final modMenu.PopupMenuPosition position;
final Offset offset;
final TextStyle? textStyle;
final EdgeInsets? padding;
final bool enable;
final void Function()? onTap;
final List<modMenu.PopupMenuEntry<T>> Function(BuildContext) itemBuilder;
final Widget child;
@override
final double height;
@override
bool represents(T? value) => false;
@override
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() =>
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>();
}
class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
extends State<W> {
@protected
void handleTap(T value) {
widget.onTap?.call();
Navigator.pop<T>(context, value);
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
TextStyle style = widget.textStyle ??
popupMenuTheme.textStyle ??
theme.textTheme.subtitle1!;
return modMenu.PopupMenuButton<T>(
enabled: widget.enable,
position: widget.position,
offset: widget.offset,
onSelected: handleTap,
itemBuilder: widget.itemBuilder,
padding: EdgeInsets.zero,
child: AnimatedDefaultTextStyle(
style: style,
duration: kThemeChangeDuration,
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: widget.height),
padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 16),
child: widget.child,
),
),
);
}
}
class MenuConfig {
// adapt to the screen height
static const fontSize = 14.0;
static const midPadding = 10.0;
static const iconScale = 0.8;
static const iconWidth = 12.0;
static const iconHeight = 12.0;
final double secondMenuHeight;
final Color commonColor;
const MenuConfig(
{required this.commonColor,
this.secondMenuHeight = kMinInteractiveDimension});
}
abstract class MenuEntryBase<T> {
modMenu.PopupMenuEntry<T> build(BuildContext context, MenuConfig conf);
}
class MenuEntryDivider<T> extends MenuEntryBase<T> {
@override
modMenu.PopupMenuEntry<T> build(BuildContext context, MenuConfig conf) {
return const modMenu.PopupMenuDivider();
}
}
typedef RadioOptionsGetter = List<Tuple2<String, String>> Function();
typedef RadioCurOptionGetter = Future<String> Function();
typedef RadioOptionSetter = Future<void> Function(String);
class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
final String text;
final RadioOptionsGetter optionsGetter;
final RadioCurOptionGetter curOptionGetter;
final RadioOptionSetter optionSetter;
final RxString _curOption = "".obs;
MenuEntrySubRadios(
{required this.text,
required this.optionsGetter,
required this.curOptionGetter,
required this.optionSetter}) {
() async {
_curOption.value = await curOptionGetter();
}();
}
List<Tuple2<String, String>> get options => optionsGetter();
RxString get curOption => _curOption;
setOption(String option) async {
await optionSetter(option);
final opt = await curOptionGetter();
if (_curOption.value != opt) {
_curOption.value = opt;
}
}
modMenu.PopupMenuEntry<T> _buildSecondMenu(
BuildContext context, MenuConfig conf, Tuple2<String, String> opt) {
return modMenu.PopupMenuItem(
padding: EdgeInsets.zero,
child: TextButton(
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.secondMenuHeight),
child: Row(
children: [
SizedBox(
width: 20.0,
height: 20.0,
child: Obx(() => opt.item2 == curOption.value
? Icon(
Icons.check,
color: conf.commonColor,
)
: SizedBox.shrink())),
const SizedBox(width: MenuConfig.midPadding),
Text(
opt.item1,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
)
],
),
),
onPressed: () {
if (opt.item2 != curOption.value) {
setOption(opt.item2);
}
},
),
);
}
@override
modMenu.PopupMenuEntry<T> build(BuildContext context, MenuConfig conf) {
return PopupMenuChildrenItem(
height: conf.secondMenuHeight,
padding: EdgeInsets.zero,
itemBuilder: (BuildContext context) =>
options.map((opt) => _buildSecondMenu(context, conf, opt)).toList(),
child: Row(children: [
const SizedBox(width: MenuConfig.midPadding),
Text(
text,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Icon(
Icons.keyboard_arrow_right,
color: conf.commonColor,
),
))
]),
);
}
}
typedef SwitchGetter = Future<bool> Function();
typedef SwitchSetter = Future<void> Function(bool);
abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
final String text;
MenuEntrySwitchBase({required this.text});
RxBool get curOption;
Future<void> setOption(bool option);
@override
modMenu.PopupMenuEntry<T> build(BuildContext context, MenuConfig conf) {
return modMenu.PopupMenuItem(
padding: EdgeInsets.zero,
child: Obx(
() => SwitchListTile(
value: curOption.value,
onChanged: (v) {
setOption(v);
},
title: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.secondMenuHeight),
child: Text(
text,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
)),
dense: true,
visualDensity: const VisualDensity(
horizontal: VisualDensity.minimumDensity,
vertical: VisualDensity.minimumDensity,
),
contentPadding: EdgeInsets.only(left: 8.0),
),
),
);
}
}
class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
final SwitchGetter getter;
final SwitchSetter setter;
final RxBool _curOption = false.obs;
MenuEntrySwitch(
{required String text, required this.getter, required this.setter})
: super(text: text) {
() async {
_curOption.value = await getter();
}();
}
@override
RxBool get curOption => _curOption;
@override
setOption(bool option) async {
await setter(option);
final opt = await getter();
if (_curOption.value != opt) {
_curOption.value = opt;
}
}
}
typedef Switch2Getter = RxBool Function();
typedef Switch2Setter = Future<void> Function(bool);
class MenuEntrySwitch2<T> extends MenuEntrySwitchBase<T> {
final Switch2Getter getter;
final SwitchSetter setter;
MenuEntrySwitch2(
{required String text, required this.getter, required this.setter})
: super(text: text);
@override
RxBool get curOption => getter();
@override
setOption(bool option) async {
await setter(option);
}
}
class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
final String text;
final List<MenuEntryBase<T>> entries;
MenuEntrySubMenu({
required this.text,
required this.entries,
});
@override
modMenu.PopupMenuEntry<T> build(BuildContext context, MenuConfig conf) {
return PopupMenuChildrenItem(
height: conf.secondMenuHeight,
padding: EdgeInsets.zero,
position: modMenu.PopupMenuPosition.overSide,
itemBuilder: (BuildContext context) =>
entries.map((entry) => entry.build(context, conf)).toList(),
child: Row(children: [
const SizedBox(width: MenuConfig.midPadding),
Text(
text,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Icon(
Icons.keyboard_arrow_right,
color: conf.commonColor,
),
))
]),
);
}
}
class MenuEntryButton<T> extends MenuEntryBase<T> {
final Widget Function(TextStyle? style) childBuilder;
Function() proc;
MenuEntryButton({
required this.childBuilder,
required this.proc,
});
@override
modMenu.PopupMenuEntry<T> build(BuildContext context, MenuConfig conf) {
return modMenu.PopupMenuItem(
padding: EdgeInsets.zero,
child: TextButton(
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.secondMenuHeight),
child: childBuilder(
const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
)),
onPressed: () {
proc();
},
),
);
}
}
class CustomMenu<T> {
final List<MenuEntryBase<T>> entries;
final MenuConfig conf;
const CustomMenu({required this.entries, required this.conf});
List<modMenu.PopupMenuEntry<T>> build(BuildContext context) {
return entries.map((entry) => entry.build(context, conf)).toList();
}
}

View File

@@ -0,0 +1,560 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:get/get.dart';
import 'package:tuple/tuple.dart';
import '../../common.dart';
import '../../mobile/widgets/dialog.dart';
import '../../mobile/widgets/overlay.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import './popup_menu.dart';
import './material_mod_popup_menu.dart' as modMenu;
class _MenubarTheme {
static const Color commonColor = MyTheme.accent;
static const double height = kMinInteractiveDimension;
}
class RemoteMenubar extends StatefulWidget {
final String id;
final FFI ffi;
const RemoteMenubar({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
@override
State<RemoteMenubar> createState() => _RemoteMenubarState();
}
class _RemoteMenubarState extends State<RemoteMenubar> {
final RxBool _show = false.obs;
final Rx<Color> _hideColor = Colors.white12.obs;
bool get isFullscreen => Get.find<RxBool>(tag: 'fullscreen').isTrue;
void setFullscreen(bool v) {
Get.find<RxBool>(tag: 'fullscreen').value = v;
}
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topCenter,
child: Obx(
() => _show.value ? _buildMenubar(context) : _buildShowHide(context)),
);
}
Widget _buildShowHide(BuildContext context) {
return SizedBox(
width: 100,
height: 5,
child: TextButton(
onHover: (bool v) {
_hideColor.value = v ? Colors.white60 : Colors.white24;
},
onPressed: () {
_show.value = !_show.value;
},
child: Obx(() => Container(
color: _hideColor.value,
))));
}
Widget _buildMenubar(BuildContext context) {
final List<Widget> menubarItems = [];
if (!isWebDesktop) {
menubarItems.add(_buildFullscreen(context));
if (widget.ffi.ffiModel.isPeerAndroid) {
menubarItems.add(IconButton(
tooltip: translate('Mobile Actions'),
color: _MenubarTheme.commonColor,
icon: Icon(Icons.build),
onPressed: () {
if (mobileActionsOverlayEntry == null) {
showMobileActionsOverlay();
} else {
hideMobileActionsOverlay();
}
},
));
}
}
menubarItems.add(_buildMonitor(context));
menubarItems.add(_buildControl(context));
menubarItems.add(_buildDisplay(context));
if (!isWeb) {
menubarItems.add(_buildChat(context));
}
menubarItems.add(_buildClose(context));
return PopupMenuTheme(
data: PopupMenuThemeData(
textStyle: TextStyle(color: _MenubarTheme.commonColor)),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Container(
color: Colors.white,
child: Row(
mainAxisSize: MainAxisSize.min,
children: menubarItems,
)),
_buildShowHide(context),
]));
}
Widget _buildFullscreen(BuildContext context) {
return IconButton(
tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'),
onPressed: () {
setFullscreen(!isFullscreen);
},
icon: Obx(() => isFullscreen
? Icon(
Icons.fullscreen_exit,
color: _MenubarTheme.commonColor,
)
: Icon(
Icons.fullscreen,
color: _MenubarTheme.commonColor,
)),
);
}
Widget _buildChat(BuildContext context) {
return IconButton(
tooltip: translate('Chat'),
onPressed: () {
widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
widget.ffi.chatModel.toggleChatOverlay();
},
icon: Icon(
Icons.message,
color: _MenubarTheme.commonColor,
),
);
}
Widget _buildMonitor(BuildContext context) {
final pi = widget.ffi.ffiModel.pi;
return modMenu.PopupMenuButton(
tooltip: translate('Select Monitor'),
padding: EdgeInsets.zero,
position: modMenu.PopupMenuPosition.under,
icon: Stack(
alignment: Alignment.center,
children: [
Icon(
Icons.personal_video,
color: _MenubarTheme.commonColor,
),
Padding(
padding: EdgeInsets.only(bottom: 3.9),
child: Obx(() {
RxInt display = CurrentDisplayState.find(widget.id);
return Text(
"${display.value + 1}/${pi.displays.length}",
style: TextStyle(color: _MenubarTheme.commonColor, fontSize: 8),
);
}),
)
],
),
itemBuilder: (BuildContext context) {
final List<Widget> rowChildren = [];
final double selectorScale = 1.3;
for (int i = 0; i < pi.displays.length; i++) {
rowChildren.add(Transform.scale(
scale: selectorScale,
child: Stack(
alignment: Alignment.center,
children: [
Icon(
Icons.personal_video,
color: _MenubarTheme.commonColor,
),
TextButton(
child: Container(
alignment: AlignmentDirectional.center,
constraints:
BoxConstraints(minHeight: _MenubarTheme.height),
child: Padding(
padding: EdgeInsets.only(bottom: 2.5),
child: Text(
(i + 1).toString(),
style: TextStyle(color: _MenubarTheme.commonColor),
),
)),
onPressed: () {
RxInt display = CurrentDisplayState.find(widget.id);
if (display.value != i) {
bind.sessionSwitchDisplay(id: widget.id, value: i);
pi.currentDisplay = i;
display.value = i;
}
},
)
],
),
));
}
return <modMenu.PopupMenuEntry<String>>[
modMenu.PopupMenuItem<String>(
height: _MenubarTheme.height,
padding: EdgeInsets.zero,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: rowChildren),
)
];
},
);
}
Widget _buildControl(BuildContext context) {
return modMenu.PopupMenuButton(
padding: EdgeInsets.zero,
icon: Icon(
Icons.bolt,
color: _MenubarTheme.commonColor,
),
tooltip: translate('Control Actions'),
position: modMenu.PopupMenuPosition.under,
itemBuilder: (BuildContext context) => _getControlMenu()
.map((entry) => entry.build(
context,
MenuConfig(
commonColor: _MenubarTheme.commonColor,
secondMenuHeight: _MenubarTheme.height,
)))
.toList(),
);
}
Widget _buildDisplay(BuildContext context) {
return modMenu.PopupMenuButton(
padding: EdgeInsets.zero,
icon: Icon(
Icons.tv,
color: _MenubarTheme.commonColor,
),
tooltip: translate('Display Settings'),
position: modMenu.PopupMenuPosition.under,
onSelected: (String item) {},
itemBuilder: (BuildContext context) => _getDisplayMenu()
.map((entry) => entry.build(
context,
MenuConfig(
commonColor: _MenubarTheme.commonColor,
secondMenuHeight: _MenubarTheme.height,
)))
.toList(),
);
}
Widget _buildClose(BuildContext context) {
return IconButton(
tooltip: translate('Close'),
onPressed: () {
clientClose(widget.ffi.dialogManager);
},
icon: Icon(
Icons.close,
color: _MenubarTheme.commonColor,
),
);
}
List<MenuEntryBase<String>> _getControlMenu() {
final pi = widget.ffi.ffiModel.pi;
final perms = widget.ffi.ffiModel.permissions;
final List<MenuEntryBase<String>> displayMenu = [];
if (pi.version.isNotEmpty) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Refresh'),
style: style,
),
proc: () {
Navigator.pop(context);
bind.sessionRefresh(id: widget.id);
},
));
}
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('OS Password'),
style: style,
),
proc: () {
Navigator.pop(context);
showSetOSPassword(widget.id, false, widget.ffi.dialogManager);
},
));
if (!isWebDesktop) {
if (perms['keyboard'] != false && perms['clipboard'] != false) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Paste'),
style: style,
),
proc: () {
Navigator.pop(context);
() async {
ClipboardData? data =
await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) {
bind.sessionInputString(id: widget.id, value: data.text ?? "");
}
}();
},
));
}
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Reset canvas'),
style: style,
),
proc: () {
Navigator.pop(context);
widget.ffi.cursorModel.reset();
},
));
}
if (perms['keyboard'] != false) {
if (pi.platform == 'Linux' || pi.sasEnabled) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Insert') + ' Ctrl + Alt + Del',
style: style,
),
proc: () {
Navigator.pop(context);
bind.sessionCtrlAltDel(id: widget.id);
},
));
}
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Insert Lock'),
style: style,
),
proc: () {
Navigator.pop(context);
bind.sessionLockScreen(id: widget.id);
},
));
if (pi.platform == 'Windows') {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Obx(() => Text(
translate(
(BlockInputState.find(widget.id).value ? 'Unb' : 'B') +
'lock user input'),
style: style,
)),
proc: () {
Navigator.pop(context);
RxBool blockInput = BlockInputState.find(widget.id);
bind.sessionToggleOption(
id: widget.id,
value: (blockInput.value ? 'un' : '') + 'block-input');
blockInput.value = !blockInput.value;
},
));
}
}
if (gFFI.ffiModel.permissions["restart"] != false &&
(pi.platform == "Linux" ||
pi.platform == "Windows" ||
pi.platform == "Mac OS")) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Restart Remote Device'),
style: style,
),
proc: () {
Navigator.pop(context);
showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager);
},
));
}
return displayMenu;
}
List<MenuEntryBase<String>> _getDisplayMenu() {
final displayMenu = [
MenuEntrySubRadios<String>(
text: translate('Ratio'),
optionsGetter: () => [
Tuple2<String, String>(translate('Original'), 'original'),
Tuple2<String, String>(translate('Shrink'), 'shrink'),
Tuple2<String, String>(translate('Stretch'), 'stretch'),
],
curOptionGetter: () async {
return await bind.sessionGetOption(
id: widget.id, arg: 'view-style') ??
'';
},
optionSetter: (String v) async {
await bind.sessionPeerOption(
id: widget.id, name: "view-style", value: v);
widget.ffi.canvasModel.updateViewStyle();
}),
MenuEntrySubRadios<String>(
text: translate('Scroll Style'),
optionsGetter: () => [
Tuple2<String, String>(translate('ScrollAuto'), 'scrollauto'),
Tuple2<String, String>(translate('Scrollbar'), 'scrollbar'),
],
curOptionGetter: () async {
return await bind.sessionGetOption(
id: widget.id, arg: 'scroll-style') ??
'';
},
optionSetter: (String v) async {
await bind.sessionPeerOption(
id: widget.id, name: "scroll-style", value: v);
widget.ffi.canvasModel.updateScrollStyle();
}),
MenuEntrySubRadios<String>(
text: translate('Image Quality'),
optionsGetter: () => [
Tuple2<String, String>(translate('Good image quality'), 'best'),
Tuple2<String, String>(translate('Balanced'), 'balanced'),
Tuple2<String, String>(
translate('Optimize reaction time'), 'low'),
],
curOptionGetter: () async {
String quality =
await bind.sessionGetImageQuality(id: widget.id) ?? 'balanced';
if (quality == '') quality = 'balanced';
return quality;
},
optionSetter: (String v) async {
await bind.sessionSetImageQuality(id: widget.id, value: v);
}),
MenuEntrySwitch<String>(
text: translate('Show remote cursor'),
getter: () async {
return await bind.sessionGetToggleOptionSync(
id: widget.id, arg: 'show-remote-cursor');
},
setter: (bool v) async {
await bind.sessionToggleOption(
id: widget.id, value: 'show-remote-cursor');
}),
MenuEntrySwitch<String>(
text: translate('Show quality monitor'),
getter: () async {
return await bind.sessionGetToggleOptionSync(
id: widget.id, arg: 'show-quality-monitor');
},
setter: (bool v) async {
await bind.sessionToggleOption(
id: widget.id, value: 'show-quality-monitor');
widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
}),
];
final perms = widget.ffi.ffiModel.permissions;
final pi = widget.ffi.ffiModel.pi;
if (perms['audio'] != false) {
displayMenu.add(_createSwitchMenuEntry('Mute', 'disable-audio'));
}
if (perms['keyboard'] != false) {
if (perms['clipboard'] != false) {
displayMenu.add(
_createSwitchMenuEntry('Disable clipboard', 'disable-clipboard'));
}
displayMenu.add(_createSwitchMenuEntry(
'Lock after session end', 'lock-after-session-end'));
if (pi.platform == 'Windows') {
displayMenu.add(MenuEntrySwitch2<String>(
text: translate('Privacy mode'),
getter: () {
return PrivacyModeState.find(widget.id);
},
setter: (bool v) async {
Navigator.pop(context);
await bind.sessionToggleOption(
id: widget.id, value: 'privacy-mode');
}));
}
}
return displayMenu;
}
MenuEntrySwitch<String> _createSwitchMenuEntry(String text, String option) {
return MenuEntrySwitch<String>(
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);
});
}
}
void showSetOSPassword(
String id, bool login, OverlayDialogManager dialogManager) async {
final controller = TextEditingController();
var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? "";
var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != "";
controller.text = password;
dialogManager.show((setState, close) {
return CustomAlertDialog(
title: Text(translate('OS Password')),
content: Column(mainAxisSize: MainAxisSize.min, children: [
PasswordWidget(controller: controller),
CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Text(
translate('Auto Login'),
),
value: autoLogin,
onChanged: (v) {
if (v == null) return;
setState(() => autoLogin = v);
},
),
]),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: () {
close();
},
child: Text(translate('Cancel')),
),
TextButton(
style: flatButtonStyle,
onPressed: () {
var text = controller.text.trim();
bind.sessionPeerOption(id: id, name: "os-password", value: text);
bind.sessionPeerOption(
id: id, name: "auto-login", value: autoLogin ? 'Y' : '');
if (text != "" && login) {
bind.sessionInputOsPassword(id: id, value: text);
}
close();
},
child: Text(translate('OK')),
),
]);
});
}

View File

@@ -172,9 +172,9 @@ class FfiModel with ChangeNotifier {
} else if (name == 'update_quality_status') {
parent.target?.qualityMonitorModel.updateQualityStatus(evt);
} else if (name == 'update_block_input_state') {
updateBlockInputState(evt);
updateBlockInputState(evt, peerId);
} else if (name == 'update_privacy_mode') {
updatePrivacyMode(evt);
updatePrivacyMode(evt, peerId);
}
};
}
@@ -231,9 +231,9 @@ class FfiModel with ChangeNotifier {
} else if (name == 'update_quality_status') {
parent.target?.qualityMonitorModel.updateQualityStatus(evt);
} else if (name == 'update_block_input_state') {
updateBlockInputState(evt);
updateBlockInputState(evt, peerId);
} else if (name == 'update_privacy_mode') {
updatePrivacyMode(evt);
updatePrivacyMode(evt, peerId);
}
};
platformFFI.setEventCallback(cb);
@@ -305,6 +305,12 @@ class FfiModel with ChangeNotifier {
_pi.sasEnabled = evt['sas_enabled'] == "true";
_pi.currentDisplay = int.parse(evt['current_display']);
try {
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
} catch (e) {
//
}
if (isPeerAndroid) {
_touchMode = true;
if (parent.target?.ffiModel.permissions['keyboard'] != false) {
@@ -343,13 +349,24 @@ class FfiModel with ChangeNotifier {
notifyListeners();
}
updateBlockInputState(Map<String, dynamic> evt) {
updateBlockInputState(Map<String, dynamic> evt, String peerId) {
_inputBlocked = evt['input_state'] == 'on';
notifyListeners();
try {
BlockInputState.find(peerId).value = evt['input_state'] == 'on';
} catch (e) {
//
}
}
updatePrivacyMode(Map<String, dynamic> evt) {
updatePrivacyMode(Map<String, dynamic> evt, String peerId) {
notifyListeners();
try {
PrivacyModeState.find(peerId).value =
bind.sessionGetToggleOptionSync(id: peerId, arg: 'privacy-mode');
} catch (e) {
//
}
}
}